TypeScript SDK
Official TypeScript SDK for the AirPinpoint asset tracking API. Install, configure, and start tracking in minutes.
The official TypeScript SDK provides a typed, ergonomic client for the AirPinpoint API. It handles authentication, pagination, retries, and error handling so you can focus on building your integration.
Installation
npm install @airpinpoint/sdk
# or
bun add @airpinpoint/sdkQuick Start
import { AirPinpoint } from "@airpinpoint/sdk";
const ap = new AirPinpoint({ apiKey: "your-api-key" });
// List all trackable devices
const { data: trackables } = await ap.trackables.list();
console.log(`Found ${trackables.length} devices`);
// Get current location
const location = await ap.trackables.currentLocation(trackables[0].id);
console.log(`${trackables[0].name}: ${location.latitude}, ${location.longitude}`);Tip
Set the AIRPINPOINT_API_KEY environment variable and the SDK will pick it up automatically. No need to pass apiKey in the constructor.
Configuration
Pass options to the AirPinpoint constructor to customize behavior.
const ap = new AirPinpoint({
apiKey: process.env.AIRPINPOINT_API_KEY,
baseURL: "https://api.airpinpoint.com/v1",
timeout: 30000,
maxRetries: 2,
});| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | AIRPINPOINT_API_KEY env var | Your API key |
baseURL | string | https://api.airpinpoint.com/v1 | API base URL |
timeout | number | 30000 | Request timeout in milliseconds |
maxRetries | number | 2 | Number of retries on transient errors |
fetch | typeof fetch | global fetch | Custom fetch implementation |
Resources: Trackables
Trackables represent your AirTags, iPhones, and other Find My devices.
List trackables
Returns all devices linked to your account.
const { data: trackables } = await ap.trackables.list({
skip: 0,
limit: 50,
});
for (const device of trackables) {
console.log(`${device.name} (${device.model}) - enabled: ${device.enabled}`);
}| Parameter | Type | Default | Description |
|---|---|---|---|
skip | number | 0 | Results to skip for pagination |
limit | number | 50 | Results per page (1-100) |
Returns: PaginatedList<TrackableDetail>
Retrieve a trackable
const device = await ap.trackables.retrieve("trk_abc123");
console.log(`${device.name} paired at ${device.pairedAt}`);Returns: TrackableDetail
Get current location
const loc = await ap.trackables.currentLocation("trk_abc123");
console.log(`Delivery Van #3: ${loc.latitude}, ${loc.longitude}`);
console.log(`Accuracy: ${loc.horizontalAccuracy}m`);
console.log(`Last seen: ${loc.timestamp}`);Returns: LocationBase
Get location history
const history = await ap.trackables.locationHistory("trk_abc123", {
startTime: "2024-01-14T00:00:00Z",
endTime: "2024-01-15T23:59:59Z",
limit: 200,
});
console.log(`${history.length} location points`);
for (const point of history) {
console.log(` ${point.timestamp}: ${point.latitude}, ${point.longitude}`);
}| Parameter | Type | Description |
|---|---|---|
startTime | string | Start of range (ISO 8601) |
endTime | string | End of range (ISO 8601) |
limit | number | Max results (default: 50) |
Returns: LocationBase[]
Get battery status
const battery = await ap.trackables.battery("trk_abc123");
console.log(`Level: ${battery.batteryLevel}`); // 0=full, 1=medium, 2=low, 3=critical
console.log(`Days remaining: ${battery.estimatedDaysRemaining}`);Returns: BatteryInfo
Reset battery
Call this after replacing an AirTag battery to reset the estimated life counter.
const result = await ap.trackables.resetBattery("trk_abc123", {
batteryMonths: 12,
});
console.log(result.message); // "Battery reset successfully"Returns: Message
Resources: Geofences
Create virtual boundaries and get notified when devices cross them.
List geofences
const { data: fences } = await ap.geofences.list({
limit: 20,
trackableId: "trk_abc123", // optional filter
});
for (const fence of fences) {
console.log(`${fence.name}: ${fence.radius}m radius at ${fence.latitude}, ${fence.longitude}`);
}| Parameter | Type | Description |
|---|---|---|
skip | number | Results to skip |
limit | number | Results per page (1-100) |
trackableId | string | Filter by device ID |
Returns: PaginatedList<GeofenceBase>
Create a geofence
const fence = await ap.geofences.create({
name: "Warehouse Zone A",
latitude: 37.7749,
longitude: -122.4194,
radius: 200,
trackableId: ["trk_abc123", "trk_def456"],
notifyType: "both",
notifyDestination: "https://yourapp.com/webhooks/geofence",
webhookSecret: "whsec_your_secret",
webhookEnabled: true,
});
console.log(`Created geofence: ${fence.id}`);Returns: GeofenceBase
Update a geofence
const updated = await ap.geofences.update("geofence_789012", {
name: "Warehouse Zone A (Expanded)",
radius: 300,
webhookEnabled: true,
});
console.log(`Updated: ${updated.name}, radius: ${updated.radius}m`);Returns: GeofenceBase
Delete a geofence
Note: the method is del, not delete (reserved word in JavaScript).
const result = await ap.geofences.del("geofence_789012");
console.log(result.message); // "Geofence deleted successfully"Returns: Message
Test a webhook
Send a test payload to your webhook endpoint to verify your integration.
const result = await ap.geofences.testWebhook("geofence_789012", {
eventType: "entry",
});
console.log(`Test delivery: ${result.deliveryId}`);Returns: { success: boolean; message: string; deliveryId: string }
List webhook deliveries
const deliveries = await ap.geofences.webhooks.list("geofence_789012", {
limit: 10,
successful: true,
});
for (const d of deliveries.data) {
console.log(`${d.id}: ${d.statusCode} at ${d.deliveredAt}`);
}| Parameter | Type | Description |
|---|---|---|
limit | number | Results per page |
successful | boolean | Filter by success status |
Get webhook delivery details
const delivery = await ap.geofences.webhooks.retrieve(
"geofence_789012",
"webhook_delivery_123456"
);
console.log(`Status: ${delivery.statusCode}`);
console.log(`Response: ${delivery.responseBody}`);Returns: WebhookDeliveryDetail
Full geofence lifecycle example
import { AirPinpoint } from "@airpinpoint/sdk";
const ap = new AirPinpoint();
// 1. Create a geofence around your job site
const fence = await ap.geofences.create({
name: "Downtown Job Site",
latitude: 40.7128,
longitude: -74.0060,
radius: 150,
trackableId: ["trk_forklift42", "trk_excavator7"],
notifyType: "both",
notifyDestination: "https://yourapp.com/webhooks/geofence",
webhookSecret: "whsec_abc123",
webhookEnabled: true,
});
// 2. Test the webhook
await ap.geofences.testWebhook(fence.id, { eventType: "entry" });
// 3. Expand the radius after surveying
const updated = await ap.geofences.update(fence.id, {
radius: 250,
});
// 4. Check delivery history
const deliveries = await ap.geofences.webhooks.list(fence.id, { limit: 5 });
console.log(`${deliveries.data.length} webhook deliveries`);
// 5. Clean up when the project ends
await ap.geofences.del(fence.id);Resources: Share Links
Generate temporary links to share a device's live location with others.
const link = await ap.shareLinks.create({
trackableId: "trk_abc123",
hours: 24, // valid for 1-168 hours
});
console.log(`Share this link: ${link.shareUrl}`);
// https://airpinpoint.com/share/8a7b6c5d-4e3f-2d1c-0b9a-8c7d6e5f4g3hReturns: { shareUrl: string }
Resources: Account
Retrieve your account information.
const account = await ap.account.retrieve();
console.log(`${account.name} (${account.email})`);
console.log(`Status: ${account.subscription_status}`);Returns: AccountInfo
Resources: Usage
Track your API usage and billing.
// Get usage breakdown for a date range
const usage = await ap.usage.retrieve({
startDate: "2024-01-01",
endDate: "2024-01-31",
});
console.log(`API calls: ${usage.apiCalls}`);
console.log(`Tracked devices: ${usage.trackedDevices}`);
// Get total usage summary
const total = await ap.usage.total({
startDate: "2024-01-01",
endDate: "2024-01-31",
});
console.log(`Total requests: ${total.totalRequests}`);Auto-Pagination
The SDK supports two pagination patterns.
Manual pagination
Fetch one page at a time with explicit control over offsets.
const page1 = await ap.trackables.list({ limit: 10, skip: 0 });
const page2 = await ap.trackables.list({ limit: 10, skip: 10 });Automatic iteration
Use for await...of to iterate through all pages automatically. The SDK fetches the next page when the current one is exhausted.
for await (const trackable of ap.trackables.list()) {
console.log(`${trackable.name}: ${trackable.enabled ? "active" : "disabled"}`);
}Note
Auto-pagination respects rate limits automatically. Each page request counts as one API call.
Error Handling
The SDK throws typed errors for different failure modes. Every error extends AirPinpointError.
import {
AirPinpoint,
AuthenticationError,
NotFoundError,
ValidationError,
RateLimitError,
InternalError,
APIConnectionError,
} from "@airpinpoint/sdk";
const ap = new AirPinpoint();
try {
const location = await ap.trackables.currentLocation("trk_invalid");
} catch (err) {
if (err instanceof AuthenticationError) {
console.error("Invalid API key. Check your AIRPINPOINT_API_KEY.");
} else if (err instanceof NotFoundError) {
console.error("Device not found. Verify the trackable ID.");
} else if (err instanceof ValidationError) {
console.error(`Bad request: ${err.message}`);
} else if (err instanceof RateLimitError) {
console.error(`Rate limited. Retry after ${err.retryAfter}s`);
} else if (err instanceof InternalError) {
console.error("Server error. Try again later.");
} else if (err instanceof APIConnectionError) {
console.error("Network error. Check your connection.");
}
}| Error Class | Status | When |
|---|---|---|
AuthenticationError | 401 | Invalid or missing API key |
NotFoundError | 404 | Resource doesn't exist |
ValidationError | 400 | Invalid request parameters |
RateLimitError | 429 | Too many requests |
InternalError | 500+ | Server error |
APIConnectionError | - | Network or timeout failure |
Tip
RateLimitError includes a retryAfter property (in seconds) so you can wait the right amount of time before retrying.
Webhook Verification
Verify that incoming webhooks are from AirPinpoint using the built-in webhooks helper.
import { webhooks } from "@airpinpoint/sdk";
const event = await webhooks.constructEvent(
rawBody, // raw request body string
signature, // X-AirPinpoint-Signature header
"whsec_your_secret" // your webhook secret
);
if (event.event === "geofence.entry") {
console.log(`${event.beacon.name} entered ${event.geofence.name}`);
}
if (event.event === "geofence.exit") {
console.log(`${event.beacon.name} left ${event.geofence.name}`);
}Next.js API Route
// app/api/webhooks/geofence/route.ts
import { webhooks } from "@airpinpoint/sdk";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("x-airpinpoint-signature");
if (!signature) {
return NextResponse.json({ error: "Missing signature" }, { status: 400 });
}
try {
const event = await webhooks.constructEvent(
rawBody,
signature,
process.env.AIRPINPOINT_WEBHOOK_SECRET!
);
if (event.event === "geofence.entry") {
// Forklift #42 arrived at Warehouse Zone A
await notifyDispatch(event.beacon.name, event.geofence.name);
}
return NextResponse.json({ received: true });
} catch (err) {
return NextResponse.json({ error: "Invalid signature" }, { status: 403 });
}
}Express
import express from "express";
import { webhooks } from "@airpinpoint/sdk";
const app = express();
app.post(
"/webhooks/geofence",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-airpinpoint-signature"] as string;
try {
const event = await webhooks.constructEvent(
req.body.toString(),
signature,
process.env.AIRPINPOINT_WEBHOOK_SECRET!
);
switch (event.event) {
case "geofence.entry":
console.log(`${event.beacon.name} entered ${event.geofence.name}`);
break;
case "geofence.exit":
console.log(`${event.beacon.name} left ${event.geofence.name}`);
break;
}
res.sendStatus(200);
} catch (err) {
res.sendStatus(403);
}
}
);Utility Functions
The SDK includes utility functions for common geospatial calculations, data analysis, and format conversion.
Geo Utilities
import { geo } from "@airpinpoint/sdk";| Function | Signature | Description |
|---|---|---|
distance | (a: Coordinates, b: Coordinates) => number | Distance in meters between two points (Haversine) |
bearing | (from: Coordinates, to: Coordinates) => number | Compass bearing in degrees (0-360) |
isInsideCircle | (point: Coordinates, center: Coordinates, radiusMeters: number) => boolean | Check if point is inside a circular area |
isInsidePolygon | (point: Coordinates, polygon: Coordinates[]) => boolean | Check if point is inside a polygon (ray casting) |
isInsideGeofence | (point: Coordinates, geofence: GeofenceBase) => boolean | Check if point is inside a geofence |
distanceToGeofence | (point: Coordinates, geofence: GeofenceBase) => number | Distance from point to nearest geofence edge (meters) |
bufferGeofence | (geofence: GeofenceBase, bufferMeters: number) => GeofenceBase | Expand or shrink a geofence boundary |
import { geo } from "@airpinpoint/sdk";
const warehouse = { latitude: 37.7749, longitude: -122.4194 };
const vanLocation = { latitude: 37.7755, longitude: -122.4188 };
// Distance between two points
const meters = geo.distance(warehouse, vanLocation);
console.log(`Van is ${meters.toFixed(0)}m from warehouse`);
// Check if a device is inside a geofence
const fence = { latitude: 37.7749, longitude: -122.4194, radius: 200 };
const inside = geo.isInsideGeofence(vanLocation, fence);
console.log(`Van inside geofence: ${inside}`);
// Bearing from warehouse to van
const heading = geo.bearing(warehouse, vanLocation);
console.log(`Van is at bearing ${heading.toFixed(0)}°`);Analysis Utilities
import { analysis } from "@airpinpoint/sdk";| Function | Signature | Description |
|---|---|---|
speed | (a: LocationBase, b: LocationBase) => number | Speed in m/s between two location points |
detectStops | (locations: LocationBase[], options?: StopOptions) => Stop[] | Find stops where device stayed in one area |
detectTrips | (locations: LocationBase[], options?: TripOptions) => Trip[] | Identify movement segments between stops |
dwellTime | (locations: LocationBase[], center: Coordinates, radiusMeters: number) => number | Total time spent in an area (seconds) |
simplifyPath | (locations: LocationBase[], toleranceMeters: number) => LocationBase[] | Reduce path points while preserving shape (Ramer-Douglas-Peucker) |
clusterLocations | (locations: LocationBase[], radiusMeters: number) => LocationCluster[] | Group nearby points into clusters |
import { AirPinpoint, analysis } from "@airpinpoint/sdk";
const ap = new AirPinpoint();
const history = await ap.trackables.locationHistory("trk_forklift42", {
startTime: "2024-01-15T06:00:00Z",
endTime: "2024-01-15T18:00:00Z",
});
// Find where the forklift stopped for more than 10 minutes
const stops = analysis.detectStops(history, {
minDurationSeconds: 600,
maxRadiusMeters: 30,
});
for (const stop of stops) {
console.log(
`Stopped at ${stop.latitude}, ${stop.longitude} ` +
`for ${(stop.durationSeconds / 60).toFixed(0)} min`
);
}
// Detect trips between stops
const trips = analysis.detectTrips(history);
for (const trip of trips) {
console.log(
`Trip: ${trip.distanceMeters.toFixed(0)}m ` +
`in ${(trip.durationSeconds / 60).toFixed(0)} min`
);
}Format Utilities
import { format } from "@airpinpoint/sdk";| Function | Signature | Description |
|---|---|---|
toGeoJSON | (locations: LocationBase[]) => GeoJSON.FeatureCollection | Convert locations to a GeoJSON FeatureCollection |
toGeoJSONPoint | (location: LocationBase) => GeoJSON.Feature<Point> | Convert a single location to a GeoJSON Point Feature |
toGeoJSONLineString | (locations: LocationBase[]) => GeoJSON.Feature<LineString> | Convert locations to a GeoJSON LineString (route) |
encodePolyline | (locations: Coordinates[]) => string | Encode coordinates as a Google Encoded Polyline |
decodePolyline | (encoded: string) => Coordinates[] | Decode a Google Encoded Polyline to coordinates |
import { AirPinpoint, format } from "@airpinpoint/sdk";
import { writeFileSync } from "fs";
const ap = new AirPinpoint();
const history = await ap.trackables.locationHistory("trk_van3", {
startTime: "2024-01-15T00:00:00Z",
endTime: "2024-01-15T23:59:59Z",
});
// Export as GeoJSON for use in Mapbox, Leaflet, QGIS, etc.
const geojson = format.toGeoJSON(history);
writeFileSync("van3-route.geojson", JSON.stringify(geojson, null, 2));
// Convert to a route line
const route = format.toGeoJSONLineString(history);
console.log(`Route with ${route.geometry.coordinates.length} points`);
// Encode as polyline for Google Maps API
const polyline = format.encodePolyline(history);
console.log(`Encoded polyline: ${polyline.substring(0, 40)}...`);Filter Utilities
import { filter } from "@airpinpoint/sdk";| Function | Signature | Description |
|---|---|---|
filterByAccuracy | (locations: LocationBase[], maxMeters: number) => LocationBase[] | Keep only points with accuracy better than threshold |
filterBySpeed | (locations: LocationBase[], maxMps: number) => LocationBase[] | Remove points implying impossible speed |
filterStale | (locations: LocationBase[], maxAgeSeconds: number) => LocationBase[] | Remove points older than threshold |
deduplicate | (locations: LocationBase[], minDistanceMeters: number) => LocationBase[] | Remove points too close to each other |
import { AirPinpoint, filter } from "@airpinpoint/sdk";
const ap = new AirPinpoint();
const raw = await ap.trackables.locationHistory("trk_excavator7", {
startTime: "2024-01-15T00:00:00Z",
endTime: "2024-01-15T23:59:59Z",
});
// GPS cleanup pipeline
const cleaned = filter.deduplicate(
filter.filterBySpeed(
filter.filterByAccuracy(raw, 50), // drop points with >50m accuracy
30 // drop points implying >30 m/s (108 km/h)
),
5 // drop points within 5m of each other
);
console.log(`Raw: ${raw.length} points, cleaned: ${cleaned.length} points`);Battery Utilities
import { battery } from "@airpinpoint/sdk";| Function | Signature | Description |
|---|---|---|
estimateBatteryDaysRemaining | (info: BatteryInfo) => number | Estimated days until battery replacement needed |
batteryHealthStatus | (info: BatteryInfo) => "good" | "fair" | "low" | "critical" | Human-readable battery health category |
import { AirPinpoint, battery } from "@airpinpoint/sdk";
const ap = new AirPinpoint();
const { data: trackables } = await ap.trackables.list();
// Check battery health across all devices
for (const device of trackables) {
const info = await ap.trackables.battery(device.id);
const status = battery.batteryHealthStatus(info);
const days = battery.estimateBatteryDaysRemaining(info);
if (status === "low" || status === "critical") {
console.log(`[!] ${device.name}: ${status}, ~${days} days remaining`);
}
}Types Reference
All types are exported from the main package.
import type {
Coordinates,
LocationBase,
TrackableDetail,
BatteryInfo,
GeofenceBase,
GeofenceCreateParams,
GeofenceUpdateParams,
WebhookEvent,
WebhookDeliveryBase,
WebhookDeliveryDetail,
PaginatedList,
AccountInfo,
UsageInfo,
ShareLinkResponse,
Message,
Stop,
Trip,
LocationCluster,
StopOptions,
TripOptions,
ClientConfig,
AirPinpointError,
} from "@airpinpoint/sdk";| Type | Description |
|---|---|
Coordinates | { latitude: number; longitude: number } |
LocationBase | Location point with timestamp, accuracy, and battery level |
TrackableDetail | Full device info including name, model, enabled status, and last location |
BatteryInfo | Battery level, last reset date, months, and estimated days remaining |
GeofenceBase | Geofence with center, radius, trackable IDs, and webhook config |
GeofenceCreateParams | Parameters for creating a geofence |
GeofenceUpdateParams | Parameters for updating a geofence (all fields optional) |
WebhookEvent | Parsed webhook payload with event type, geofence, beacon, and location |
WebhookDeliveryBase | Webhook delivery summary (ID, status, timestamp) |
WebhookDeliveryDetail | Full delivery details including response body and headers |
PaginatedList<T> | { data: T[]; total: number; skip: number; limit: number } |
AccountInfo | Account details and subscription status |
UsageInfo | API usage breakdown for a date range |
ShareLinkResponse | { shareUrl: string } |
Message | { message: string } |
Stop | Detected stop with location, duration, and time range |
Trip | Detected trip with distance, duration, and waypoints |
LocationCluster | Group of nearby locations with center and point count |
StopOptions | Config for stop detection (min duration, max radius) |
TripOptions | Config for trip detection (min distance, min duration) |
ClientConfig | Constructor options for AirPinpoint |
AirPinpointError | Base error class for all SDK errors |