Skip to main content

Android 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

Requires location permission (plus Notifications on Android 13+ and Bluetooth on Android 12+). For background operation, also requires ACCESS_BACKGROUND_LOCATION ("Allow all the time") — always a separate, second request after foreground location is granted. Without it, geofence registration is skipped entirely (startRouteGeofencing logs a warning and no geofences are set). See Runtime Permissions below, or the Permissions page for buckets, states and the staged flow.

How It Works

  1. You pass the route as a List<DeliveryStop>; the SDK runs a foreground service for its lifetime and geofences the nearest stops.
  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), tightens location cadence near stops to balance precision and battery, and transparently manages more stops than Android's 100-geofence cap. These are tuned via remote config, not in your app.

API Reference

FunctionDescription
startRouteGeofencing(stops, options, callback)Begins geofencing for stops (List<DeliveryStop>), tuned by options (RouteGeofenceOptions). Fails if stops is empty or the SDK isn't initialized/eligible (see Runtime Permissions)
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, callback)Replaces the active stop set (List<DeliveryStop>) mid-route and re-diffs registration. A session for a removed stop is auto-stopped (tagged removed_from_route)
stopRouteGeofencing()Clears all geofences, stops active route sessions, ends the foreground service
resumeRouteGeofencingIfNeeded()Restores geofencing from persisted state. Call on every app launch; no-op if no route is active
monitoredStopsProperty. Returns the stops currently registered (List<DeliveryStop>, empty if no route is active)
geofenceSessionEventsProperty. A hot Flow<GeofenceSessionEvent> of auto start/stop events (replays the latest to new subscribers)

1. Initialize SDK

Initialize the SDK in your main Activity, typically in onCreate(), then set your API key once initialization succeeds.

MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.DoorstepPermission
import com.doorstepai.sdks.tracking.DoorstepPermissionState

class MainActivity : ComponentActivity() {
companion object {
private const val FIXED_API_TOKEN = SOME_API_KEY_REF // Reference from an ENV
}

private var permissionsGranted by mutableStateOf(false)
private var sdkInitialized by mutableStateOf(false)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissionsAndInitialize()
setContent { /* Your Compose UI */ }
}

fun checkPermissionsAndInitialize() {
// checkPermission never prompts — see Runtime Permissions below.
val location = DoorstepAI.checkPermission(this, DoorstepPermission.LOCATION_WHEN_IN_USE)
permissionsGranted = location.state == DoorstepPermissionState.GRANTED
if (permissionsGranted) {
initializeSDK()
} else {
DoorstepAI.requestPermissions(this, setOf(DoorstepPermission.LOCATION_WHEN_IN_USE))
}
}

private fun initializeSDK() {
DoorstepAI.init(
context = this,
notificationTitle = "Tracking...",
notificationText = "Tracking your delivery"
) { result ->
result.fold(
onSuccess = {
DoorstepAI.setAPIKey(FIXED_API_TOKEN)
sdkInitialized = true
},
onFailure = { error -> sdkInitialized = false }
)
}
}
}

init parameters

ParameterTypeRequiredDescription
contextContextYesApplication context
notificationTitleString?NoTitle on the foreground-service notification shown while tracking
notificationTextString?NoDescription on that notification
callback(Result<Unit>) -> UnitYesInitialization result callback

setAPIKey parameters

ParameterTypeDefaultDescription
keyStringn/aJWT sent as Authorization: Bearer <key>
shouldGetConfigBooleantrueWhen true, fetch config immediately. When false, defer until the first startDeliveryByX call
// Defer config fetch until the first delivery starts:
DoorstepAI.setAPIKey(FIXED_API_TOKEN, shouldGetConfig = false)
API Key Security

Store your API key securely using BuildConfig fields, 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:

import com.doorstepai.sdks.tracking.DoorstepAI

DoorstepAI.startRouteGeofencing(
stops = myRouteStops, // List<DeliveryStop>
options = myGeofenceOptions // RouteGeofenceOptions(), defaults if omitted
) { result ->
result.fold(
onSuccess = { Log.i("DoorstepAI", "Geofencing started") },
onFailure = { error -> Log.e("DoorstepAI", "Failed to start geofencing: ${error.message}") }
)
}

3. Mark Drop-off

Mark the drop-off when the driver takes a POD or confirms delivery in-app. markDropoff is a suspend function — call it from a coroutine scope. It returns no Result; failures are logged internally via SDKLogger.

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).

import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

lifecycleScope.launch {
DoorstepAI.markDropoff(
deliveryId = "delivery_12345",
dropoffType = DropoffType.POD // or DropoffType.NON_POD
)
}

markDropoff parameters

ParameterTypeDescription
deliveryIdStringRequired. The session you're marking
dropoffTypeDropoffTypePOD (proof of delivery captured) or NON_POD

Custom Events

Use newEvent to record a custom event on a delivery. It comes in both a callback and a suspend variant. Pass an optional timestamp to backdate the event; omit it and the event is recorded at "now".

// Callback variant
DoorstepAI.newEvent(
eventName = "taking_pod",
deliveryId = "delivery_12345"
) { result -> /* same fold as above */ }

// Suspend variant, recorded at an explicit time
lifecycleScope.launch {
DoorstepAI.newEvent(
eventName = "taking_pod",
deliveryId = "delivery_12345",
timestamp = 1720000000.0 // epoch seconds
)
}

newEvent parameters

ParameterTypeDescription
eventNameStringRequired. The event to record
deliveryIdStringRequired. The session the event belongs to
timestampDouble?Epoch seconds the event occurred. When provided, the event is recorded at that time instead of "now". Defaults to null (now)

4. Stop Geofencing

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

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:

DoorstepAI.updateRouteStops(
stops = updatedRouteStops // List<DeliveryStop>
) { result ->
result.fold(
onSuccess = { Log.i("DoorstepAI", "Stops updated") },
onFailure = { error -> Log.e("DoorstepAI", "Failed to update stops: ${error.message}") }
)
}

Resuming After Relaunch

Not part of the standard flow above, but worth adding for reliability: route state is persisted, so geofencing survives process death and OS restarts. Call resumeRouteGeofencingIfNeeded() on every app launch, right after init succeeds, 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:

private fun initializeSDK() {
DoorstepAI.init(
context = this,
notificationTitle = "Tracking...",
notificationText = "Tracking your delivery"
) { result ->
result.fold(
onSuccess = {
DoorstepAI.setAPIKey(FIXED_API_TOKEN)
DoorstepAI.resumeRouteGeofencingIfNeeded()
},
onFailure = { error -> /* handle init failure */ }
)
}
}

Getting Monitored Stops

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

val stops = DoorstepAI.monitoredStops
Log.i("DoorstepAI", "${stops.size} stop(s) currently monitored")

Types

DeliveryStop

A single stop on the route. deliveryId is used as both the session's clientSessionId and the geofence request id: the same id you pass to markDropoff and stopDelivery.

@Serializable
data class DeliveryStop(
val deliveryId: String,
val address: String,
val latitude: Double,
val longitude: Double,
val radiusMeters: Double? = null,
val customerId: String? = null,
val driverId: String? = null
)
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 null
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) { … } works with no options.

@Serializable
data class RouteGeofenceOptions(
val defaultRadiusMeters: Double = 250.0,
val timeoutSeconds: Double? = null,
val autoStopAfterDropoffSeconds: Double? = null,
val manualForeground: Boolean = false
)
FieldTypeDefaultDescription
defaultRadiusMetersDouble250.0Geofence radius for stops that don't set their own radiusMeters
timeoutSecondsDouble?nullBackstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. null uses remote config
autoStopAfterDropoffSecondsDouble?nullForwarded to the post-dropoff auto-stop (see Auto-Stop After Dropoff)
manualForegroundBooleanfalseWhen true, your app owns the foreground service and the SDK won't promote its own. See Manual Foreground Service

GeofenceSessionEvent & GeofenceSessionEventType

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

data class GeofenceSessionEvent(
val deliveryId: String,
val type: GeofenceSessionEventType,
val reason: String,
val serverSessionId: String? = null
)

enum class GeofenceSessionEventType { STARTED, STOPPED }
FieldTypeDescription
deliveryIdStringThe stop's DeliveryStop.deliveryId
typeGeofenceSessionEventTypeSTARTED (session auto-started on ENTER) or STOPPED (session auto-stopped)
reasonStringWhat triggered it: geofence, distance, timeout, removed_from_route, route_cleared, or manual
serverSessionIdString?Server-assigned session id, populated on STARTED once session creation succeeds. null on STOPPED, and null on STARTED if the session was created in CSV-only mode (no server round-trip)

Observing Geofencing Events

Collect geofenceSessionEvents to subscribe to auto start/stop events so your UI can reflect them in real time. It's a hot Flow<GeofenceSessionEvent> that replays the latest value to new subscribers:

lifecycleScope.launch {
DoorstepAI.geofenceSessionEvents.collect { event ->
Log.i("DoorstepAI", "Geofencing event: ${event.deliveryId} -> ${event.type} (${event.reason})")
}
}

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
Permissions

Tracking needs runtime location permission. The full permission flow (Compose launcher + DoorstepAIPermissionUtils) is documented under Runtime Permissions below.


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 — not once inside the building. Pass a unique deliveryId you can correlate on your side, and handle the Result in the callback (invalid key, denied permissions, etc.).

Pick whichever address format you have:

import com.doorstepai.sdks.tracking.AddressType
import com.doorstepai.sdks.tracking.LatLngObject

// By Google Place ID (fold once to show the callback shape)
DoorstepAI.startDeliveryByPlaceID(
placeID = "some_place_id",
deliveryId = "delivery_12345"
) { result ->
result.fold(
onSuccess = { message -> updateDeliveryStatus(DeliveryStatus.ACTIVE) },
onFailure = { error -> showErrorToUser(error.message) }
)
}

// By address components
val address = AddressType(
streetNumber = "123",
route = "Main Street",
subPremise = "Apt 4B",
locality = "San Francisco",
administrativeAreaLevel1 = "CA",
postalCode = "94102"
)
DoorstepAI.startDeliveryByAddressType(
address = address,
deliveryId = "delivery_12345"
) { result -> /* same fold as above */ }

// By single address string, with optional coordinates and knobs
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.0
) { result -> /* same fold as above */ }

Start parameters

Every start method takes a deliveryId plus these optional knobs:

ParameterTypeDescription
deliveryIdStringRequired. Unique per session; correlate it on your side
timeoutSecondsDouble?Auto-stops tracking after this duration, a backstop if the exit geofence is missed
manualForegroundBooleanWhen true, the SDK won't promote its TrackingService to the foreground; your app must already run its own. Defaults to false. See Foreground Service
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:

DoorstepAI.startDeliveryByPlusCode(
plusCode = "some_plus_code",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0
) { /* result handler */ }

DoorstepAI.startDeliveryByLatLng(
latitude = 37.7749,
longitude = -122.4194,
subUnit = "Apt 4B",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0
) { /* result handler */ }

The deprecated Plus Code and lat/lng overloads do not expose manualForeground. Use the supported place/address starts when you need that option.


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.


4. Stop Tracking

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

try {
DoorstepAI.stopDelivery("delivery_12345")
} catch (e: Exception) {
Log.e("DoorstepAI", "Error stopping delivery: ${e.message}")
}

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


Runtime Permissions

Tracking needs foreground location before initializing (or before the first start). Background collection and route geofencing additionally need background location, which Android 11+ auto-denies when it is bundled with the foreground ask — so it is always a separate, second request, made only after foreground location is granted. Background location is only half of it: collection also needs a running foreground service to survive the app being backgrounded. checkPermissions observes without prompting; requestPermissions asks for exactly the buckets you whitelist and nothing else.

Which prompts appear

PermissionPromptWhen
Location (fine + coarse)System location dialog (Precise/Approximate toggle on Android 12+)When requested
POST_NOTIFICATIONSNotifications dialogAndroid 13+ (API 33)
BLUETOOTH_SCAN"Nearby devices" dialogAndroid 12+ (API 31)
ACCESS_BACKGROUND_LOCATION"Allow all the time" (a Settings screen on Android 11+)Requested separately, after foreground location

INTERNET, ACCESS_NETWORK_STATE, FOREGROUND_SERVICE, WAKE_LOCK, and the legacy BLUETOOTH/BLUETOOTH_ADMIN permissions are install-time (normal) permissions — they're granted automatically and never prompt, so they don't belong in a runtime request.

MainActivity.kt
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.DoorstepPermission
import com.doorstepai.sdks.tracking.DoorstepPermissionState

// Observe — never prompts. LOCATION_ALWAYS reports WHEN_IN_USE_ONLY when only
// foreground location is held: background tracking and route geofencing won't work then.
fun backgroundReady(): Boolean {
val states = DoorstepAI.checkPermissions(this)
return states[DoorstepPermission.LOCATION_ALWAYS]?.state == DoorstepPermissionState.GRANTED
}

// Ask 1 — foreground location, e.g. from your "Start shift" button.
fun onStartShiftTapped() {
DoorstepAI.requestPermissions(this, setOf(DoorstepPermission.LOCATION_WHEN_IN_USE))
}

// Ask 2 — background location, from a LATER, separate user action (after your own
// "why background location" screen) and only once foreground location is granted.
fun onBackgroundRationaleAccepted() {
val foreground = DoorstepAI.checkPermission(this, DoorstepPermission.LOCATION_WHEN_IN_USE)
if (foreground.state != DoorstepPermissionState.GRANTED) return
DoorstepAI.requestPermissions(this, setOf(DoorstepPermission.LOCATION_ALWAYS))
}

Keep those two asks in separate call sites, not two lines in a row: issued in the same turn, the second one sees foreground location as not-yet-granted and comes back with LOCATION_ALWAYS in deferred instead of prompting. The staged permission flow shows the full four-stage version.

Forward every answer so the SDK can tell a re-askable DENIED from a Settings-only PERMANENTLY_DENIED:

MainActivity.kt
override fun onRequestPermissionsResult(rc: Int, perms: Array<String>, results: IntArray) {
super.onRequestPermissionsResult(rc, perms, results)
DoorstepAI.notePermissionRequestResult(this, perms, results)
checkPermissionsAndInitialize() // re-read state, then initialize or ask for background next
}
Full permission reference

Buckets, states, the staged permission flow and troubleshooting are on the Permissions page.

Background location must be requested separately

Never bundle LOCATION_ALWAYS into the same requestPermissions call as LOCATION_WHEN_IN_USE — on Android 11+ (API 30) the system silently ignores it. Request foreground location first, then request LOCATION_ALWAYS alone once it's granted.


Advanced APIs

Foreground Service

A running foreground service is what keeps collection alive once the driver leaves your app. Android freezes a backgrounded process that has no foreground service, and collection stops with it — background location permission alone does not prevent that. Getting the FGS up, and confirming it came up, is therefore part of a correct integration.

By default (manualForeground = false) the SDK owns this for you: startDelivery* launches its TrackingService, which immediately promotes itself with a location-type foreground service and posts the tracking notification. Title and text come from init; the FOREGROUND_SERVICE and FOREGROUND_SERVICE_LOCATION permissions arrive through the SDK's library manifest.

Confirm the service actually started

Promotion can be refused, and the SDK reports that rather than failing the call, so check it once during integration:

  • The tracking notification appears in the shade for the whole session. It is the user-visible proof. POST_NOTIFICATIONS must be granted for it to be shown.

  • Ask the OS, which is the definitive check:

    adb shell dumpsys activity services com.doorstepai.sdks.tracking.internal.TrackingService \
    | grep -iE 'isForeground|fgRequired'
  • Watch the logadb logcat -s TrackingService -s DoorstepAI. These lines mean it did not come up:

    Log lineCause
    Foreground start rejected by Android policyForegroundServiceStartNotAllowedException — on Android 12+ you cannot start an FGS from the background. Start the delivery while your app is in the foreground
    Failed to promote service to foregroundAnything else, with the exception attached
    manualForeground=true but no host foreground service is currently runningYou opted out of the SDK's FGS but yours is not up — see below
Start deliveries from the foreground

On Android 12+ (API 31) an app in the background may not start a foreground service, and on API 29+ startDelivery* refuses outright — failing its callback with "Cannot start tracking while app is backgrounded without background location permission" — if the app is backgrounded and ACCESS_BACKGROUND_LOCATION is not granted. Start the session while the driver is looking at your app; once the FGS is up, backgrounding is fine.

manualForeground = true — your app owns the service

Pass it when your app already runs a shift-long foreground service, so the driver does not get two tracking notifications. The SDK then starts TrackingService with startService() and never calls startForeground() — it relies entirely on yours to keep the process alive.

That makes it your obligation, and the SDK will not rescue you: if no host FGS is running it logs the warning above and continues, so tracking simply dies when the app is backgrounded.

// 1. Your foreground service must ALREADY be running — and promoted.
ContextCompat.startForegroundService(this, Intent(this, MyShiftService::class.java))
// (inside MyShiftService.onCreate: startForeground(id, notification, FOREGROUND_SERVICE_TYPE_LOCATION))

// 2. Then hand the delivery to the SDK.
DoorstepAI.startDeliveryByAddressString(
address = "123 Main St",
deliveryId = "delivery_12345",
manualForeground = true,
) { result -> /* … */ }

// 3. Keep your service alive for the whole session — stopping it stops collection.

Checklist for manualForeground = true:

  1. Your service is running and promoted with startForeground() before startDelivery*.
  2. It declares android:foregroundServiceType="location" and your manifest keeps FOREGROUND_SERVICE_LOCATION — without the location type, Android withholds location while backgrounded even though the service lives.
  3. It stays up until after stopDelivery.
  4. You verified the warning line above is absent from logcat.

The flag is persisted to SharedPreferences, so it survives TrackingService being recreated by the OS in a fresh process.

For route geofencing the equivalent knob is RouteGeofenceOptions.manualForeground. Leave it false and the SDK's route service is the foreground service for the whole route, including the sessions it auto-starts on ENTER; set it true and the same obligation above applies for as long as the route is active.

Auto-Stop After Dropoff

Pass autoStopAfterDropoffSeconds on startDeliveryByX(...). After the next markDropoff(...), the SDK schedules a Doze-safe AlarmManager alarm that fires stopDelivery(...) when the timer elapses.

  • Host-supplied value always wins over remote config.
  • null → falls back to remote config (MiscConfiguration.autoStopAfterDropoffSeconds).
  • <= 0 → no auto-stop scheduled.

Observing Config Load

DoorstepAI.configFetched is a hot Flow<Long> that emits the wall-clock timestamp (ms) of every successful config landing. It replays the most recent value, so a late subscriber sees the current state immediately.

lifecycleScope.launch {
DoorstepAI.configFetched.collect { timestampMs ->
Log.i("DoorstepAI", "Config fetched at $timestampMs")
}
}

Best Practices

  • Always handle the Result. fold over success/failure on every startDelivery* callback, and branch on error type (network, permission, etc.) to show the right message. markDropoff/newEvent's suspend variants have no Result, so wrap them in try/catch if you need to react to failures in your UI.
  • Update UI on the main thread. SDK callbacks may arrive on background threads, so wrap UI updates in runOnUiThread { … }.
  • Let tracking run across lifecycle changes. The SDK keeps tracking through onPause/onDestroy. Don't stop a delivery just because an Activity is destroyed. 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 Complete Examples: full implementation examples
  3. 🛠️ Troubleshooting Guide: common integration and runtime issues