Skip to main content

Official Python SDK for the AirPinpoint asset tracking API

Project description

AirPinpoint Python SDK

Official Python SDK for the AirPinpoint asset tracking API. Track Apple AirTags and Find My compatible devices with type-safe, production-grade Python.

Installation

pip install airpinpoint

Requires Python 3.9+.

Quick Start

from airpinpoint import AirPinpoint

# Reads AIRPINPOINT_API_KEY from environment, or pass it directly
client = AirPinpoint(api_key="your-api-key")

# List all trackables
page = client.trackables.list()
for trackable in page.data:
    print(f"{trackable.name}: {trackable.enabled}")

Authentication

Get your API key from the AirPinpoint Dashboard under API Keys.

# Option 1: Pass directly
client = AirPinpoint(api_key="your-api-key")

# Option 2: Environment variable
# export AIRPINPOINT_API_KEY=your-api-key
client = AirPinpoint()

Usage

Trackables

# List all trackables
page = client.trackables.list()

# Get a single trackable
trackable = client.trackables.retrieve("trackable-id")

# Get current location
location = client.trackables.current_location("trackable-id")
print(f"Lat: {location.latitude}, Lng: {location.longitude}")

# Get location history
from datetime import datetime
history = client.trackables.location_history(
    "trackable-id",
    start_time=datetime(2026, 1, 1),
    end_time=datetime(2026, 4, 1),
    limit=100,
)
for loc in history.data:
    print(f"{loc.timestamp}: ({loc.latitude}, {loc.longitude})")

# Battery info
battery = client.trackables.battery("trackable-id")
print(f"Battery: {battery.battery_percentage}%")

# Reset battery tracking
client.trackables.reset_battery("trackable-id", battery_months=12)

Auto-Pagination

All list methods return paginated results. Iterate directly to auto-paginate:

# Auto-paginate through all trackables
for trackable in client.trackables.list():
    print(trackable.name)

# Or access the first page manually
page = client.trackables.list(limit=10)
print(page.data)       # list of items
print(page.has_more)   # True if more pages exist

Geofences

# List geofences
geofences = client.geofences.list()

# Create a geofence
geofence = client.geofences.create(
    name="Office",
    latitude=37.7749,
    longitude=-122.4194,
    radius=500,
    trackable_id=["trackable-id-1", "trackable-id-2"],
    webhook_enabled=True,
    webhook_secret="your-secret",
)

# Update (only sends fields you provide)
from airpinpoint import NOT_GIVEN
updated = client.geofences.update(
    "geofence-id",
    name="New Office",
    radius=1000,
)

# Delete
client.geofences.delete("geofence-id")

# Test webhook
result = client.geofences.test_webhook("geofence-id", event_type="entry")
print(result.success)

# List webhook deliveries
deliveries = client.geofences.webhooks.list("geofence-id")
for d in deliveries.data:
    print(f"{d.event_type}: {'OK' if d.successful else 'FAILED'}")

# Get delivery detail
detail = client.geofences.webhooks.retrieve("geofence-id", "delivery-id")
print(detail.payload)

Share Links

link = client.share_links.create(trackable_id="trackable-id", hours=24)
print(link.share_url)

Account & Usage

user = client.account.retrieve()
print(f"Logged in as: {user.email}")

usage = client.usage.retrieve(start_date="2026-01-01", end_date="2026-04-01")
total = client.usage.total()

Async Client

Every method has an async equivalent:

import asyncio
from airpinpoint import AsyncAirPinpoint

async def main():
    async with AsyncAirPinpoint(api_key="your-api-key") as client:
        # All the same methods, just awaited
        trackables = await client.trackables.list()
        for t in trackables.data:
            print(t.name)

        # Async auto-pagination
        async for trackable in await client.trackables.list():
            print(trackable.name)

        location = await client.trackables.current_location("trackable-id")
        print(f"{location.latitude}, {location.longitude}")

asyncio.run(main())

Webhook Verification

Verify incoming webhooks from AirPinpoint:

from airpinpoint import Webhook, WebhookSignatureError

try:
    event = Webhook.construct_event(
        payload=request.body,                            # raw request body
        signature=request.headers["X-AirPinpoint-Signature"],
        secret="your-webhook-secret",
    )
    print(f"Event: {event.event}")  # "geofence.entry" or "geofence.exit"
    print(f"Beacon: {event.beacon}")
    print(f"Location: {event.location}")
except WebhookSignatureError as e:
    print(f"Invalid signature: {e}")

Utility Functions

Location-aware helper functions for distance calculation, path analysis, geofencing, and data formatting.

from airpinpoint import utils

# Distance and bearing between two locations
d = utils.distance(location_a, location_b)         # meters
b = utils.bearing(location_a, location_b)           # degrees [0, 360)

# Geofence checks
inside = utils.is_inside_geofence(location, geofence)
dist = utils.distance_to_geofence(location, geofence)

# Polygon containment (ray-casting)
inside = utils.is_inside_polygon(location, polygon_vertices)

# Speed between timestamped locations
s = utils.speed(loc1, loc2)                         # m/s

# Path analysis
stops = utils.detect_stops(locations, radius_m=50, min_duration_ms=300_000)
trips = utils.detect_trips(locations)
dwell = utils.dwell_time(locations, geofence)       # milliseconds
simplified = utils.simplify_path(locations, tolerance_m=10)
clusters = utils.cluster_locations(locations, radius_m=100)

# GeoJSON export
geojson = utils.to_geojson(locations)               # FeatureCollection
point = utils.to_geojson_point(location)            # Feature<Point>
line = utils.to_geojson_line_string(locations)       # Feature<LineString>

# Google Encoded Polyline
encoded = utils.encode_polyline(locations)
decoded = utils.decode_polyline(encoded)             # list of (lat, lng)

# Filter noisy data
clean = utils.filter_by_accuracy(locations, max_accuracy_m=100)
clean = utils.filter_by_speed(locations, max_speed_mps=100)
clean = utils.filter_stale(locations, max_age_ms=3_600_000)
clean = utils.deduplicate(locations, distance_threshold_m=1, time_threshold_ms=1000)

# Battery estimation
days = utils.estimate_battery_days_remaining(battery_info)
status = utils.battery_health_status(battery_info)  # "good" | "low" | "critical" | "replace"

All utility functions accept duck-typed objects. Anything with .latitude and .longitude attributes works, including SDK types like LocationBase and GeofenceBase.

Error Handling

from airpinpoint import (
    AirPinpoint,
    AirPinpointError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

client = AirPinpoint(api_key="your-api-key")

try:
    trackable = client.trackables.retrieve("invalid-id")
except NotFoundError as e:
    print(f"Not found: {e.message}")
except AuthenticationError:
    print("Invalid API key")
except RateLimitError:
    print("Too many requests, slow down")
except ValidationError as e:
    print(f"Bad request: {e.message}")
except AirPinpointError as e:
    print(f"SDK error: {e}")

Error hierarchy:

AirPinpointError
  APIError (status_code, message, body, headers)
    AuthenticationError (401)
    NotFoundError (404)
    ValidationError (400)
    RateLimitError (429)
    InternalError (500+)
  APIConnectionError
  WebhookSignatureError

Configuration

import httpx

client = AirPinpoint(
    api_key="your-api-key",
    base_url="https://api.airpinpoint.com/v1",  # default
    timeout=httpx.Timeout(connect=5, read=30, write=30, pool=30),
    max_retries=2,                                # retries on 429/5xx
    default_headers={"X-Custom": "value"},
)

Context Manager

Both clients support context managers for clean resource management:

# Sync
with AirPinpoint(api_key="...") as client:
    trackables = client.trackables.list()

# Async
async with AsyncAirPinpoint(api_key="...") as client:
    trackables = await client.trackables.list()

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

airpinpoint-0.1.0.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

airpinpoint-0.1.0-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

File details

Details for the file airpinpoint-0.1.0.tar.gz.

File metadata

  • Download URL: airpinpoint-0.1.0.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for airpinpoint-0.1.0.tar.gz
Algorithm Hash digest
SHA256 83e10256027f77e664563766e7b285e8f5cecb55587c1db8862f5f7fb997f03b
MD5 484a9badf4f411924e049bf004121568
BLAKE2b-256 9ef46095016439a3eef4f2125e359b2c0459239905c7a55019e94f8bd4dd58b3

See more details on using hashes here.

Provenance

The following attestation bundles were made for airpinpoint-0.1.0.tar.gz:

Publisher: publish-python-sdk.yml on bhaktatejas922/airtag-api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file airpinpoint-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: airpinpoint-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 27.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for airpinpoint-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d335c617cf1deccb8c3d06da485051ca39d8f29ee006bae0bdbbcd68bb36bf7b
MD5 580bb2c177e371dfae403eddf8c34fb0
BLAKE2b-256 832cc52b75e42897185157046638e43f75b4edf7cef03425de18c848dd787604

See more details on using hashes here.

Provenance

The following attestation bundles were made for airpinpoint-0.1.0-py3-none-any.whl:

Publisher: publish-python-sdk.yml on bhaktatejas922/airtag-api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page