Skip to main content

iOS SDK Usage & Integration

In-SDK Geofencing is the recommended way to integrate: hand the SDK your stops and it starts and stops tracking automatically as the driver enters and exits each one. Prefer to drive geofence enter/exit yourself? See Manual Geofencing below.

In-SDK Geofencing

Hand the SDK your stops and it auto-starts and stops a tracking session as the driver enters and exits each one.

This flow has four touchpoints in your driver app (Initialize SDK is shared with the manual flow below):

#When this happens in the driver appWhat you call
1Driver app launchesInitialize SDK
2Start geofencing and tracking all stopsStart Geofencing
3Driver confirms drop-off or takes proof of deliveryMark Drop-off
4Stop geofencing and tracking all stopsStop Geofencing

If the driver's route changes mid-shift, call Update Stops as well.

Permissions

Tracking needs location permission, and Always ("Change to Always Allow") is what locked-phone and backgrounded tracking — plus route geofencing — actually require: with When-In-Use only, iOS stops delivering fixes shortly after the screen locks. The full flow (the checkPermissions / requestPermissions whitelist API, the eight states, and a staged prompt recipe) is on iOS Permissions.

How It Works

  1. You pass the route as an array of DeliveryStop; the SDK geofences the nearest stops for the route's lifetime.
  2. On ENTER, it auto-starts a session for that stop (same as startDeliveryByAddressString).
  3. You call markDropoff when the driver completes the stop.
  4. On EXIT, the session stops only if markDropoff was called. Otherwise it runs until timeoutSeconds, so a false EXIT can't cut a delivery short.
  5. Every auto-start/stop emits a GeofenceSessionEvent for your UI.

The SDK handles the rest automatically: it absorbs GPS jitter (enter/exit hysteresis + debounce) and tightens location cadence near stops to balance precision and battery. These are tuned via remote config, not in your app.

API Reference

FunctionDescription
startRouteGeofencing(stops:options:)Begins geofencing for stops ([DeliveryStop]), tuned by options (RouteGeofenceOptions). Throws if stops is empty or the SDK isn't initialized/eligible
markDropoff(deliveryId:dropoffType:)Marks a stop's drop-off, gating its geofence EXIT stop. Shared with the manual flow (see Mark Drop-off)
updateRouteStops(stops:)Replaces the active stop set ([DeliveryStop]) mid-route and re-diffs registration. A session for a removed stop is auto-stopped (tagged removed_from_route)
stopRouteGeofencing()Clears all geofences and stops active route sessions
resumeRouteGeofencingIfNeeded()Restores geofencing from persisted state. Call on every app launch; no-op if no route is active
monitoredStopsProperty. Returns the stops currently registered ([DeliveryStop], empty if no route is active)
addGeofenceSessionListener(_:)Subscribe to auto start/stop (GeofenceSessionEvent) events. Returns a subscription; call .remove() to unsubscribe

1. Initialize SDK

Initialize the SDK once, early in your app's lifecycle, typically in your App struct or AppDelegate.

MyApp.swift
import SwiftUI
import DoorstepDropoffSDK

@main
struct MyApp: App {
init() {
// Initialize DoorstepAI with your API key.
// The SDK kicks off a config fetch immediately by default.
DoorstepAI.setApiKey(key: "YOUR_API_KEY_HERE")
}

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

setApiKey parameters

ParameterTypeDefaultDescription
keyStringn/aJWT issued by DoorstepAI; sent as Authorization: Bearer <key>
shouldGetConfigBooltrueWhen true, fetch config immediately. When false, defer until the first startDelivery* call

Request permissions

Keep foreground and background location as separate, user-visible stages. Request the other collection buckets when your UI has explained their purpose:

@MainActor
func askForForegroundTrackingPermissions() {
DoorstepAI.requestPermissions([
.locationWhenInUse,
.motionFitness,
.bluetooth
])
}

@MainActor
func askForBackgroundTrackingAfterRationale() {
// Call from a later "Continue" action after When In Use is granted.
DoorstepAI.requestPermissions([.locationAlways])
}

Call each stage from a user-initiated onboarding or "Start shift" action—not from init(), app launch, or mid-delivery. The calls are @MainActor; a SwiftUI button action already runs in the right context. iOS controls when it offers the final Always upgrade, so re-check the detailed .locationAlways state after each phase.

Need to ask for buckets individually, read the result, or check state without prompting? See iOS Permissions — it covers the whitelist form, every state, and the staged prompt flow. The old DoorstepAI.requestAllPermissions(...) is the superseded compatibility requester and still works unchanged. New integrations should use the typed whitelist API above.

API Key Security

Store your API key securely using environment variables or a secure configuration service. Never hardcode API keys in production builds.


2. Start Geofencing

Call startRouteGeofencing once you have the driver's stops for the shift. Pass every stop up front, not one at a time:

Task {
do {
try await DoorstepAI.startRouteGeofencing(
stops: myRouteStops, // [DeliveryStop]
options: myGeofenceOptions // RouteGeofenceOptions(), defaults if omitted
)
} catch {
// Handle empty stops or an ineligible SDK state
}
}

3. Mark Drop-off

When the driver completes the delivery (takes a POD or confirms drop-off in-app), mark it. markDropoff is the preferred API:

The call is identical for both flows. With In-SDK Geofencing, marking drop-off also gates the automatic geofence EXIT stop for that delivery: the session keeps running until it's called (or the timeout backstop fires).

Task {
do {
try await DoorstepAI.markDropoff(
deliveryId: "delivery_12345",
dropoffType: .pod // or .non_pod
)
} catch {
// Handle error
}
}

markDropoff parameters

ParameterTypeDescription
deliveryIdStringRequired. The session you're marking
dropoffTypeDropoffType.pod (proof of delivery captured) or .non_pod

Custom events with newEvent

Beyond drop-offs, newEvent tags any custom event on the active session (for example, when the driver starts capturing a photo):

Task {
try await DoorstepAI.newEvent(
eventName: "taking_pod",
deliveryId: "delivery_12345",
timestamp: 1720000000 // optional epoch seconds; omit for "now"
)
}
ParameterTypeDescription
eventNameStringRequired. Name of the event to record on the session
deliveryIdStringRequired. The active session to attach the event to
timestampDouble?Epoch seconds for the event; omit it to use the current time

4. Stop Geofencing

Call stopRouteGeofencing when the shift ends (or the driver logs out). It clears every geofence and stops any sessions still running:

Task {
await DoorstepAI.stopRouteGeofencing()
}

Update Stops

Call updateRouteStops when the driver's route changes mid-shift (a stop added, removed, or reordered). It re-diffs against the currently monitored stops instead of tearing everything down, and auto-stops a session for any stop dropped from the list, tagged removed_from_route:

Task {
do {
try await DoorstepAI.updateRouteStops(stops: updatedRouteStops) // [DeliveryStop]
} catch {
// Handle error
}
}

Resuming After Relaunch

Not part of the standard flow above, but worth adding for reliability: route state is persisted, so geofencing survives process termination and OS restarts. Call resumeRouteGeofencingIfNeeded() on every app launch to restore it without re-supplying the stop list. It's a no-op if no route is active, so it's safe to call unconditionally:

@main
struct MyApp: App {
init() {
DoorstepAI.setApiKey(key: "YOUR_API_KEY_HERE")
DoorstepAI.resumeRouteGeofencingIfNeeded()
}

var body: some Scene {
WindowGroup { ContentView() }
}
}

Getting Monitored Stops

Read monitoredStops to get the stops currently registered, useful for rehydrating your UI on launch or confirming a route is active:

let stops = DoorstepAI.monitoredStops
print("\(stops.count) stop(s) currently monitored")

Types

DeliveryStop

A single stop on the route. deliveryId is the same id you pass to markDropoff and stopDelivery.

struct DeliveryStop {
let deliveryId: String
let address: String
let latitude: Double
let longitude: Double
let radiusMeters: Double?
let customerId: String?
let driverId: String?
}
FieldTypeRequiredDescription
deliveryIdStringYesYour id for the stop. Pass the same id to markDropoff/stopDelivery. Duplicates are de-duped (last wins)
addressStringYesPassed verbatim to session creation
latitudeDoubleYesStop latitude (geofence center)
longitudeDoubleYesStop longitude (geofence center)
radiusMetersDouble?NoPer-stop geofence radius. Falls back to RouteGeofenceOptions.defaultRadiusMeters / remote config when nil
customerIdString?NoForwarded to session creation for correlation on your backend
driverIdString?NoForwarded to session creation for correlation on your backend

RouteGeofenceOptions

Tuning for the whole route. Every field has a default, so startRouteGeofencing(stops: stops) works with no options.

struct RouteGeofenceOptions {
var defaultRadiusMeters: Double = 250
var timeoutSeconds: Int? = nil
var autoStopAfterDropoffSeconds: Int? = nil
}
FieldTypeDefaultDescription
defaultRadiusMetersDouble250Geofence radius for stops that don't set their own radiusMeters
timeoutSecondsInt?nilBackstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. nil uses remote config
autoStopAfterDropoffSecondsInt?nilForwarded to the post-dropoff auto-stop, same as startDelivery*'s autoStopAfterDropoffSeconds

GeofenceSessionEvent & GeofenceSessionEventType

Emitted whenever the SDK auto-starts or auto-stops a session in response to a geofence transition.

struct GeofenceSessionEvent {
let deliveryId: String
let type: GeofenceSessionEventType
let reason: String
}

enum GeofenceSessionEventType {
case started
case stopped
}
FieldTypeDescription
deliveryIdStringThe stop's DeliveryStop.deliveryId
typeGeofenceSessionEventType.started (session auto-started on ENTER) or .stopped (session auto-stopped)
reasonStringWhat triggered it: geofence, distance, timeout, removed_from_route, route_cleared, or manual

Observing Geofencing Events

Subscribe with addGeofenceSessionListener to receive auto start/stop events so your UI can reflect them in real time. It returns a subscription; call .remove() to unsubscribe:

let subscription = DoorstepAI.addGeofenceSessionListener { event in
print("Geofencing event: \(event.deliveryId) -> \(event.type) (\(event.reason))")
}

// Later, e.g. in deinit:
subscription.remove()

Manual Geofencing

If you'd rather drive geofence enter/exit yourself instead of using In-SDK Geofencing above, wire up these four touchpoints:

The DoorstepAI SDK has four touchpoints in your driver app. Wire up each one and you're done. Everything else on this page is reference detail for those four calls.

#When this happens in the driver appWhat you call
1App launchesInitialize SDK
2Driver enters the ≥250 m delivery geofenceStart Tracking
3Driver takes POD or confirms drop-off in-appMark Drop-off
4Driver exits the ≥250 m delivery geofenceStop Tracking

1. Initialize SDK

Initialize the SDK the same way as described in Initialize SDK above. This step is identical for both flows.


2. Start Tracking

Call startDelivery… when the driver enters the delivery geofence around the target building, not once inside it. Use a unique deliveryId per session: a meaningful identifier you can correlate on your side. Wrap calls in do/catch to handle failures such as an invalid key or denied permissions.

Pick whichever address format you have (startDeliveryByPlaceID, startDeliveryByAddressType, or startDeliveryByAddressString):

Task {
do {
// By Google Place ID
try await DoorstepAI.startDeliveryByPlaceID(
placeID: "some_place_id",
deliveryId: "delivery_12345"
)

// By address components
let address = AddressType(
streetNumber: "123",
route: "Main Street",
subPremise: "Apt 4B",
locality: "San Francisco",
administrativeAreaLevel1: "CA",
postalCode: "94102"
)
try await DoorstepAI.startDeliveryByAddressType(
address: address,
deliveryId: "delivery_12345"
)

// By single address string, with optional coordinates and knobs
try await DoorstepAI.startDeliveryByAddressString(
address: "123 Main St, Apt 4B, San Francisco, CA 94102",
deliveryId: "delivery_12345",
coordinates: LatLngObject(lat: 37.7749, lng: -122.4194),
timeoutSeconds: 1200
)
} catch {
// Handle invalid key, denied permissions, or bad input
}
}

Start parameters

Every start method takes a deliveryId plus these optional knobs:

ParameterTypeDescription
deliveryIdStringRequired. Unique per session; correlate it on your side
timeoutSecondsTimeInterval?Auto-stops tracking after this duration, a backstop if the exit geofence is missed
coordinatesLatLngObject?(AddressType / address-string variants only) pairs a textual address with a lat/lng you resolved upstream
customerIdString?Optional customer identifier passed through to session creation, for correlation on your backend
driverIdString?Optional driver identifier passed through to session creation, for correlation on your backend
Deprecated start methods

startDeliveryByPlusCode and startDeliveryByLatLng are deprecated. Use startDeliveryByAddressString / startDeliveryByAddressType with coordinates instead. They remain for backwards compatibility:

try await DoorstepAI.startDeliveryByPlusCode(
plusCode: "some_plus_code",
deliveryId: "delivery_12345",
timeoutSeconds: 1200
)

try await DoorstepAI.startDeliveryByLatLng(
latitude: 37.7749,
longitude: -122.4194,
subUnit: "Apt 4B",
deliveryId: "delivery_12345",
timeoutSeconds: 1200
)

3. Mark Drop-off

Mark the drop-off the same way as described in Mark Drop-off above. This step is identical for both flows. newEvent there also accepts an optional timestamp (epoch seconds) to backdate a custom event; omit it to use the current time.


4. Stop Tracking

Call stopDelivery when the driver exits the delivery geofence surrounding the building, not while inside it:

Task {
await DoorstepAI.stopDelivery(deliveryId: "delivery_12345")
}

That covers the full lifecycle. The sections below are reference for the calls above.


Advanced APIs

Forward background URLSession completion handlers

The SDK owns background upload sessions. Forward the handler from your app delegate together with the session identifier so iOS can finish the correct session:

AppDelegate.swift
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
DoorstepAI.setBackgroundSessionCompletionHandler(
completionHandler,
forSessionIdentifier: identifier
)
}

The identifier-less setBackgroundSessionCompletionHandler(_:) overload remains supported for existing integrations, but the identifier form is preferred because the SDK owns more than one background URL session.

SwiftUI root component (legacy)

You can render the SDK root component for legacy support. This is no longer required for new integrations; tracking starts when you call startDelivery*.

ContentView.swift
import SwiftUI
import DoorstepDropoffSDK

struct ContentView: View {
var body: some View {
VStack {
Text("Welcome to My Delivery App")
DoorstepAIRoot() // optional legacy root component
}
}
}

Best Practices

  • Always handle errors. Wrap startDelivery* / markDropoff in do/catch and surface invalid-key, denied-permission, and bad-input failures to the user.
  • Update UI on the main thread. SDK calls run in Tasks; hop back to the main actor before touching UI.
  • Let tracking run in the background. The SDK handles backgrounding automatically, so don't stop a delivery just because the app is backgrounded. Only call stopDelivery on geofence exit (or rely on timeoutSeconds as a backstop).

Next Steps

  1. 🔐 Review Permissions: buckets, states, and staged prompts
  2. 💡 View Examples: complete implementation examples with error handling