...

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/sdk

Quick 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,
});
OptionTypeDefaultDescription
apiKeystringAIRPINPOINT_API_KEY env varYour API key
baseURLstringhttps://api.airpinpoint.com/v1API base URL
timeoutnumber30000Request timeout in milliseconds
maxRetriesnumber2Number of retries on transient errors
fetchtypeof fetchglobal fetchCustom 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}`);
}
ParameterTypeDefaultDescription
skipnumber0Results to skip for pagination
limitnumber50Results 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}`);
}
ParameterTypeDescription
startTimestringStart of range (ISO 8601)
endTimestringEnd of range (ISO 8601)
limitnumberMax 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}`);
}
ParameterTypeDescription
skipnumberResults to skip
limitnumberResults per page (1-100)
trackableIdstringFilter 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}`);
}
ParameterTypeDescription
limitnumberResults per page
successfulbooleanFilter 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);

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-8c7d6e5f4g3h

Returns: { 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 ClassStatusWhen
AuthenticationError401Invalid or missing API key
NotFoundError404Resource doesn't exist
ValidationError400Invalid request parameters
RateLimitError429Too many requests
InternalError500+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";
FunctionSignatureDescription
distance(a: Coordinates, b: Coordinates) => numberDistance in meters between two points (Haversine)
bearing(from: Coordinates, to: Coordinates) => numberCompass bearing in degrees (0-360)
isInsideCircle(point: Coordinates, center: Coordinates, radiusMeters: number) => booleanCheck if point is inside a circular area
isInsidePolygon(point: Coordinates, polygon: Coordinates[]) => booleanCheck if point is inside a polygon (ray casting)
isInsideGeofence(point: Coordinates, geofence: GeofenceBase) => booleanCheck if point is inside a geofence
distanceToGeofence(point: Coordinates, geofence: GeofenceBase) => numberDistance from point to nearest geofence edge (meters)
bufferGeofence(geofence: GeofenceBase, bufferMeters: number) => GeofenceBaseExpand 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";
FunctionSignatureDescription
speed(a: LocationBase, b: LocationBase) => numberSpeed 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) => numberTotal 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";
FunctionSignatureDescription
toGeoJSON(locations: LocationBase[]) => GeoJSON.FeatureCollectionConvert 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[]) => stringEncode 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";
FunctionSignatureDescription
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";
FunctionSignatureDescription
estimateBatteryDaysRemaining(info: BatteryInfo) => numberEstimated 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";
TypeDescription
Coordinates{ latitude: number; longitude: number }
LocationBaseLocation point with timestamp, accuracy, and battery level
TrackableDetailFull device info including name, model, enabled status, and last location
BatteryInfoBattery level, last reset date, months, and estimated days remaining
GeofenceBaseGeofence with center, radius, trackable IDs, and webhook config
GeofenceCreateParamsParameters for creating a geofence
GeofenceUpdateParamsParameters for updating a geofence (all fields optional)
WebhookEventParsed webhook payload with event type, geofence, beacon, and location
WebhookDeliveryBaseWebhook delivery summary (ID, status, timestamp)
WebhookDeliveryDetailFull delivery details including response body and headers
PaginatedList<T>{ data: T[]; total: number; skip: number; limit: number }
AccountInfoAccount details and subscription status
UsageInfoAPI usage breakdown for a date range
ShareLinkResponse{ shareUrl: string }
Message{ message: string }
StopDetected stop with location, duration, and time range
TripDetected trip with distance, duration, and waypoints
LocationClusterGroup of nearby locations with center and point count
StopOptionsConfig for stop detection (min duration, max radius)
TripOptionsConfig for trip detection (min distance, min duration)
ClientConfigConstructor options for AirPinpoint
AirPinpointErrorBase error class for all SDK errors