Skip to content

Config

Config is an immutable Dart class — every field is nullable, and leaving a field unset (rather than passing null explicitly — there’s no difference, but omitting it reads better) means it’s simply left out of the native payload, so the persisted/native default applies. It’s applied in two ways:

  • ready(config) — called once, typically at app launch. It merges config over the persisted config from the previous session (or the SDK defaults documented on this page, on first run) and, if tracking was left enabled, auto-resumes it.
  • setConfig(config) — merges a partial or full config into the live tracker at any time afterwards.

Most keys take effect immediately on setConfig(). Two documented exceptions on this page — locationFilterPolicy and kalmanProfile — are only applied when the location filter is rebuilt, which happens at tracking start/stop, not live on setConfig(). If you need a filter-tuning change to take effect immediately, call setConfig() then stop()/start().

This page covers every documented Config field: Permissions, Geolocation, Motion & activity recognition, HTTP, Persistence, Authorization, Application, Logging & debug, and Geofencing. Field-level dartdoc comments in lib/src/config.dart are the source these pages are generated from — this page and that file are checked for drift on every docs build.

All keys at a glance

KeyDefaultOne-liner
locationAuthorizationRequest'Always'Authorization level to request: 'Always' or 'WhenInUse'.
locationAuthorizationAlertnone (disabled unless set)Strings for the native “insufficient permission” dialog.
disableLocationAuthorizationAlertfalseSuppresses the locationAuthorizationAlert dialog.
backgroundPermissionRationalenoneNo-op — accepted for API compatibility only.
desiredAccuracyeffectively HIGH when unsetAccuracy tier requested while moving.
distanceFilter10Minimum metres between fixes while moving.
disableElasticityfalsePins distanceFilter to its base value regardless of speed.
elasticityMultiplier1.0Scales how fast distanceFilter grows with speed.
stationaryRadius200Radius (metres) of the stationary geofence/wake region.
stationaryKeepAlivetrueKeeps a low-power location stream alive while stationary.
stationaryDesiredAccuracy'BALANCED'Accuracy tier for the stationary keep-alive stream.
stationaryDistanceFilter75iOS: displacement gate for the stationary stream.
stationaryLocationUpdateInterval30000Android: interval for the stationary stream.
locationUpdateInterval1000Android: fused-location request interval while moving.
useSessionEnginetrueiOS 17+: selects the modern session-based delivery path.
showsBackgroundLocationIndicatorfalseiOS: shows the OS background-location pill (Always; session-path hiding in beta).
disableLocationFilterfalseBypasses the Kalman/accuracy/teleport filter entirely.
locationFilterMaxAccuracy100Accuracy gate (metres) — worse fixes rejected outright.
locationFilterMaxSpeed60Teleport-rejection speed threshold, metres/second.
locationFilterPolicy'Conservative'Teleport-decision policy: Conservative / Adjust / PassThrough.
kalmanProfile'DEFAULT'Kalman smoothing tuning.
odometerAccuracyThreshold0 (off)Accuracy gate specifically for the odometer.
stopTimeout5Minutes of stillness before committing to stationary.
motionTriggerDelay0Debounce before trusting a moving activity classification.
triggerActivities"in_vehicle,on_bicycle,on_foot,running,walking"Activity types that count as moving.
minimumActivityRecognitionConfidence75 (Android) · 50 (iOS)Minimum activity-recognition confidence to trust.
activityRecognitionInterval10000Android: poll interval for Activity Recognition.
disableMotionActivityUpdatesfalseIgnores activity-recognition updates entirely.
urlnone (persist-only mode)Upload endpoint.
method'POST'HTTP verb for uploads.
headers{}Extra headers on every upload/log-flush request.
params{}Merged into the request body root.
extras{}Merged into every uploaded record’s own extras.
httpRootProperty'location'Body key that carries the location payload.
autoSynctrueAuto-uploads whenever a new record is queued.
autoSyncThreshold0Queue depth that triggers an auto-sync.
disableAutoSyncOnCellularfalseDefers auto-sync while the active network is cellular.
batchSyncfalseUploads multiple queued records in a single request.
maxBatchSize-1 (unlimited)Cap on records per batchSync request.
httpTimeoutMs30000Connect/read/write timeout for uploads and log flushes.
maxDaysToPersist2 (engine fallback)Age-based pruning of queued records, in days.
maxRecordsToPersist-1 (unlimited)Count cap on the offline queue, evicting oldest rows.
authorizationnoneNative, killed-app-safe JWT bearer-token refresh config.
stopOnTerminatefalseWhether tracking stops when the app process is terminated.
startOnBootfalseWhether tracking resumes automatically after a reboot.
heartbeatInterval60Seconds between heartbeat events.
preventSuspendfalseiOS: holds a background task to extend backgrounded life.
foregroundServicen/aNo-op — accepted for API compatibility only.
notificationsee tableAndroid: configures the foreground-service notification.
debugfalsePlays an audible cue per tracking event, both platforms.
logLevel0 (OFF)Gates what’s persisted to the native log store.
logMaxDays3 (min 1)Retention for persisted log rows, in days.
logUrlnone (local-only)Endpoint for batched native log upload.
diagnosticExtrasfalseTest devices only: adds a diagnostic snapshot to uploads.
geofenceProximityRadius1000Radius (metres) used for proximity-slicing OS-registered geofences.
maxMonitoredGeofences-1 (platform budget as-is)Cap on how many geofences are registered with the OS.
geofenceInitialTriggerEntrytrueRequests a synthetic ENTER for already-inside geofences.

Permissions

See the permissions guide for the store-manifest declarations these keys assume are already in place.

locationAuthorizationRequest

Type: String? · Default: 'Always'

The authorization level the SDK asks for: 'Always' or 'WhenInUse' (case-insensitive; any other value is treated as 'WhenInUse'). Leaving this unset behaves exactly like 'Always' — both engines default to wanting Always when the key is absent.

This key drives two things:

  • requestPermission()’s staged Android chain only requests ACCESS_BACKGROUND_LOCATION when this is 'Always'; with 'WhenInUse' the chain stops after foreground location + activity recognition. On iOS, this key decides whether an already-granted When In Use authorization is treated as sufficient ('WhenInUse', resolves immediately) or insufficient ('Always', a further Always prompt or Settings nudge is needed).
  • It gates whether an AuthorizedWhenInUse status counts as “not enabled enough” for locationAuthorizationAlert’s Settings-nudge dialog.

Set this to 'WhenInUse' only if your use case genuinely doesn’t need background tracking — with the SDK default ('Always'), a device stuck at When-In-Use will keep re-prompting/nudging.

await bg.BackgroundGeolocation.ready(bg.Config(
locationAuthorizationRequest: 'Always',
// ...
));

locationAuthorizationAlert

Type: Map<String, String>? · Default: none (alert disabled unless set)

A dictionary of strings used to build the native “your location permission isn’t sufficient” dialog, shown automatically after a requestPermission() call (or an app-foreground authorization re-check) leaves authorization insufficient per locationAuthorizationRequest, or leaves device Location Services switched off entirely. Recognized keys (all optional, each falls back to a hardcoded English default if omitted):

KeyShown when
titleWhenOffLocation Services are off at the OS level.
titleWhenNotEnabledLocation Services are on but app authorization is insufficient.
instructionsBody text under the title (empty by default).
cancelButtonDismiss button label (default "Cancel").
settingsButtonButton that deep-links to the app’s Settings page (default "Settings").

If this key is left unset entirely, no dialog is ever shown — this is opt-in, not a default nag screen. Suppress it after presenting your own onboarding UI with disableLocationAuthorizationAlert.

await bg.BackgroundGeolocation.ready(bg.Config(
locationAuthorizationRequest: 'Always',
locationAuthorizationAlert: {
'titleWhenNotEnabled': 'Background location required',
'instructions': 'This app tracks your location in the background. Please allow "Always" in Settings.',
'cancelButton': 'Not now',
'settingsButton': 'Open Settings',
},
));

disableLocationAuthorizationAlert

Type: bool? · Default: false

Suppresses the locationAuthorizationAlert dialog entirely, even if that key is configured. Use this once your app has its own onboarding flow that already explains and requests background location, so the SDK doesn’t show a second, redundant prompt.

backgroundPermissionRationale

Type: Map<String, String>? · Default: none

No-op

Accepted for API compatibility but currently does nothing on either platform: no-op on iOS, and on Android the OS’s own permission-rationale flow (shouldShowRequestPermissionRationale) is used instead of a config-driven dialog. See Limitations.

Geolocation

These keys govern GPS behavior across the moving/stationary state machine — see Tracking lifecycle for how they fit together.

desiredAccuracy

Type: int? · Default: effectively HIGH accuracy when unset

One of the desiredAccuracy* constants (a lower/negative number requests better accuracy, larger numbers coarser and cheaper fixes). Governs the accuracy tier requested from the OS location provider while moving; the stationary state has its own tier — see stationaryDesiredAccuracy. Leaving it unset is treated as the highest-accuracy tier on both platforms.

await bg.BackgroundGeolocation.ready(bg.Config(
desiredAccuracy: bg.desiredAccuracyHigh,
));

distanceFilter

Type: double? · Units: metres · Default: 10

The minimum displacement between fixes while moving. This is the primary lever for point density vs. battery/data usage — smaller values produce a denser, smoother route at the cost of more GPS wake-ups and more uploaded records; larger values sample less often. Unless disableElasticity is set, the effective filter distance is scaled by current speed — see elasticityMultiplier — so distanceFilter is best understood as the base value at low speed, not a fixed sampling interval.

A battery-saving profile that trades point density for battery, useful for long-running background tracking where a precise route isn’t critical:

await bg.BackgroundGeolocation.ready(bg.Config(
distanceFilter: 50, // coarser base spacing
elasticityMultiplier: 1.5, // scale it up further at speed
stationaryRadius: 300, // wider stop-detection radius, fewer false departures
));

disableElasticity

Type: bool? · Default: false

Pins distanceFilter to its configured base value regardless of speed, disabling the speed-elastic scaling described under elasticityMultiplier. Useful when you want predictable, constant-distance sampling for a controlled comparison or a fixed-cadence use case, at the cost of oversampling at highway speed (or undersampling at walking speed) relative to the elastic default.

elasticityMultiplier

Type: double? · Default: 1.0

Scales how aggressively distanceFilter grows with speed (ignored if disableElasticity is true). Values above 1.0 widen the gap between fixes faster as speed increases (fewer points on a highway drive, more battery/data savings); values below 1.0 flatten the curve, keeping spacing closer to the flat distanceFilter value even at speed. See the distanceFilter battery-saving example above for a combined profile.

stationaryRadius

Type: double? · Units: metres · Default: 200

The radius of the stationary geofence: once the motion state machine decides the device is parked, tracking arms this radius around the last stopped location as both (a) the wake trigger — a raw fix that clears the radius immediately re-engages tracking, faster than waiting on activity recognition — and (b) the OS-level wake-region on iOS, which is what allows the app to be relaunched by the OS after a kill or force-quit once the device leaves the radius. A larger radius trades a slower departure-wake for fewer false re-arms in a large parking lot or driveway; a smaller radius wakes sooner but is more prone to spurious wakes from GPS jitter while genuinely parked. See Boot & killed-app behavior for how this interacts with relaunch.

stationaryKeepAlive

Type: bool? · Default: true

Keeps a low-power, coarse location request alive while stationary instead of letting GPS fully sleep. This trades a small amount of stationary battery drain for a much faster wake at trip start (the stream already has a rough fix to detect displacement from, rather than needing to cold-start GPS). Setting this false restores a fully-sleeping-GPS stationary state — lower battery draw while parked, but slower to notice departure.

stationaryDesiredAccuracy

Type: String? · Default: 'BALANCED'

Accuracy tier for the stationary keep-alive stream (see stationaryKeepAlive): 'HIGH' | 'BALANCED' | 'LOW'. Deliberately never drops to a cell-tower-grade 'LOW'-equivalent fix by default — a coarse fix (hundreds to thousands of metres of error) would be rejected wholesale by the locationFilterMaxAccuracy gate, leaving the stationary wake path with no usable input to detect departure at all. Raise to 'HIGH' only if you’ve confirmed the battery cost is acceptable for your use case — it is not, by itself, a guarantee of a faster departure wake.

stationaryDistanceFilter

Type: double? · Units: metres · Default: 75

iOS

The displacement gate applied to the low-power stationary keep-alive stream on iOS (see stationaryKeepAlive) — independent of, and typically smaller than, stationaryRadius. No-op on Android, where the stationary stream’s cadence is instead governed by stationaryLocationUpdateInterval.

stationaryLocationUpdateInterval

Type: int? · Units: milliseconds · Default: 30000

Android

The fused-location (Android’s Fused Location Provider, part of Play Services) request interval for the stationary keep-alive stream on Android — the time-based counterpart to iOS’s distance-based stationaryDistanceFilter. No-op on iOS.

locationUpdateInterval

Type: int? · Units: milliseconds · Default: 1000

Android

The fused-location request interval while moving on Android. Because Android’s fused provider delivers at most one fix per this interval regardless of distanceFilter, a coarse interval silently caps point density even with a tight distanceFilter — at highway speed a 5000 ms interval alone would space points roughly 5 seconds apart no matter how small distanceFilter is set. No-op on iOS, which has no equivalent request-interval knob (Core Location delivers based on displacement).

useSessionEngine

Type: bool? · Default: true

iOS 17+

Selects the modern iOS location delivery path — CLLocationUpdate.liveUpdates plus CLBackgroundActivitySession — instead of the legacy CLLocationManager.startUpdatingLocation request, which iOS aggressively suspends between significant-location-change wakes in the background. The trade-off: the session path’s CLBackgroundActivitySession keeps the blue background-location indicator visible whenever tracking is enabled; with Always authorization plus showsBackgroundLocationIndicator: false the engine now skips that session to hide the pill (beta — see that key). This key exists purely as a remote-config kill-switch: it defaults to true and should only be set to false to force the legacy path (e.g. while diagnosing a regression suspected to be session-engine-specific). Devices on iOS below 17 always use the legacy path regardless of this flag. Android silently ignores this key (stored but unread) — it’s iOS-only.

// Emergency rollback to the legacy delivery path via remote config,
// without an app release.
await bg.BackgroundGeolocation.setConfig(bg.Config(useSessionEngine: false));

showsBackgroundLocationIndicator

Type: bool? · Default: false

iOS

Controls the blue “app is using your location in the background” pill while tracking with Always authorization. On the legacy delivery path (useSessionEngine: false, and always on iOS below 17) it maps straight to CLLocationManager.showsBackgroundLocationIndicator (Apple QA1965 semantics). On the default session path CLBackgroundActivitySession shows the indicator unconditionally — the pill is that API’s keep-alive mechanism — so there false + Always makes the engine skip creating the activity session entirely, resting background delivery on Always plus the location background mode. Beta: that session-path suppression still needs field verification that CLLocationUpdate.liveUpdates keeps delivering in the background and across eviction relaunches without the activity session (Apple doesn’t document it); until then useSessionEngine: false remains the battle-tested way to hide the pill. Two OS rules no setting can override: with When-In-Use authorization the pill is always shown, and the small status-bar location arrow is never suppressible. No-op on Android, which has no equivalent system indicator (the foreground-service notification is the closest analogue there — see the Application section).

disableLocationFilter

Type: bool? · Default: false

Bypasses the Kalman smoothing + accuracy gate + teleport-rejection filter entirely — every raw fix the OS delivers is accepted as-is. Useful for debugging (comparing raw vs. filtered output) but not recommended for production: without the accuracy gate, cell-tower-grade fixes with hundreds of metres of error are accepted into the route and the odometer.

locationFilterMaxAccuracy

Type: double? · Units: metres · Default: 100

The accuracy gate: fixes reporting worse accuracy than this are rejected by the filter outright, in every locationFilterPolicy (this gate applies even under 'PassThrough', which otherwise skips teleport rejection and Kalman smoothing). Lowering it rejects more borderline fixes (cleaner route, but more silence in weak-signal conditions like indoors or dense urban canyons); raising it admits noisier fixes. No-op if disableLocationFilter is true.

locationFilterMaxSpeed

Type: double? · Units: metres/second · Default: 60

The teleport-rejection threshold: the maximum speed implied between two consecutive fixes before the newer one is treated as a GPS jump rather than real movement. 60 m/s is ~216 km/h — generous enough for normal driving, tight enough to catch the multi-hundred-metre single-fix jumps that dense urban GPS reflection sometimes produces. How a violation is handled depends on locationFilterPolicy: dropped ('Conservative'), capped to this speed instead of dropped ('Adjust'), or ignored entirely ('PassThrough').

locationFilterPolicy

Type: String? · Default: 'Conservative'

Selects the filter’s teleport-decision phase (case-insensitive):

  • 'Conservative' (default) — a fix that implies a speed above locationFilterMaxSpeed is dropped outright. Safest choice for a route that must never show impossible jumps, at the cost of occasionally discarding a real fast-but-legitimate fix (e.g. a GPS fix taken right after a long tunnel).
  • 'Adjust' — a teleporting fix is capped to the kinematic limit instead of dropped, so the point still lands in the route (nudged back toward the previous fix) rather than vanishing. Prefer this when you’d rather see an approximate point than a gap in the route.
  • 'PassThrough' — only the locationFilterMaxAccuracy gate applies; there is no teleport rejection and no Kalman smoothing at all. Use this for debugging the raw filtered-vs-unfiltered difference, or if your own backend already does its own smoothing/outlier rejection.

Applied when the filter is rebuilt at tracking start/stop — changing it via setConfig() on a live tracker has no effect until the next stop()/ start() cycle.

// Prefer an approximate point over a gap in the route (e.g. delivery/fleet
// tracking, where a visible route matters more than strict fidelity).
await bg.BackgroundGeolocation.setConfig(bg.Config(locationFilterPolicy: 'Adjust'));
await bg.BackgroundGeolocation.stop();
await bg.BackgroundGeolocation.start(); // rebuild the filter with the new policy

kalmanProfile

Type: String? · Default: 'DEFAULT'

Selects the Kalman filter’s smoothing tuning (case-insensitive): 'DEFAULT' | 'AGGRESSIVE' (responds faster to real direction/speed changes, at the cost of a noisier-looking path) | 'CONSERVATIVE' (maximum smoothing, at the cost of more lag following a sudden turn or stop). Like locationFilterPolicy, this is applied when the filter is rebuilt at tracking start/stop — not live on setConfig().

odometerAccuracyThreshold

Type: double? · Units: metres · Default: 0 (off)

A separate, odometer-only accuracy gate: fixes with accuracy worse than this threshold are still accepted into the route and uploaded normally, but they do not advance the running odometer total. 0 (the default) disables this gate — every accepted fix advances the odometer regardless of its accuracy. Raise this if you’ve observed odometer drift from noisy-but-still- filter-passing fixes (e.g. urban multipath) and want a tighter bar specifically for distance accounting, without also tightening locationFilterMaxAccuracy and losing those fixes from the route entirely.

Motion & activity recognition

These keys tune the automatic motion-state machine described in Tracking lifecycle.

stopTimeout

Type: int? · Units: minutes · Default: 5

How long the device must be continuously classified as still before the motion state machine commits to the stationary state (arms stationaryRadius, lets GPS drop to the stationary power profile). A shorter timeout saves battery sooner after a real stop but risks false stops during traffic lights or short stops on a route; a longer timeout is more tolerant of brief stops but keeps GPS at moving-power cost longer. The countdown does not restart if already armed, and is cancelled by either a confirmed non-still activity or a raw displacement large enough to indicate the device never actually stopped.

motionTriggerDelay

Type: int? · Units: milliseconds · Default: 0

A debounce delay before a moving-type activity classification is trusted and acted on (transitioning out of stationary). 0 (the default) acts immediately on the first qualifying activity update. Raise this if you’re seeing spurious brief “moving” blips (e.g. being jostled while parked) trigger a false departure; the tradeoff is a slower real departure wake.

triggerActivities

Type: String? · Default: "in_vehicle,on_bicycle,on_foot,running,walking"

A comma-separated list of activity-recognition type names that count as “moving” for the motion state machine (recognized names: in_vehicle, on_bicycle, on_foot, running, walking; still and unknown are never treated as moving). Unrecognized names in the CSV are silently dropped. Narrow this list if a particular activity type is producing false departures for your use case — e.g. dropping on_foot if brief walking around a parked vehicle shouldn’t count as a trip start.

await bg.BackgroundGeolocation.ready(bg.Config(
triggerActivities: 'in_vehicle,on_bicycle,running',
));

minimumActivityRecognitionConfidence

Type: int? · Default: 75 (Android) · 50 (iOS)

Minimum confidence (0-100) an activity-recognition update must carry before it’s trusted by the motion state machine. The two platforms’ scales aren’t directly comparable, which is why the defaults differ: Android’s Activity Recognition API reports a genuine 0-100 confidence, so 75 is a real three-quarters bar. iOS’s Core Motion only reports a coarse low/medium/high tier, which this SDK maps onto the same 0-100 scale as 33/66/100 — so iOS’s default of 50 is deliberately pitched to sit between the low and medium tiers, i.e. “at least medium confidence”, not a literal half-confidence bar. Raising this makes the classifier more conservative (fewer false moving/stationary transitions, but slower to react to a real one); lowering it reacts faster at the cost of more noise.

activityRecognitionInterval

Type: int? · Units: milliseconds · Default: 10000

Android

How often the Android Activity Recognition client is polled for an update. No-op on iOS, where Core Motion activity updates are push-delivered rather than polled on an interval.

disableMotionActivityUpdates

Type: bool? · Default: false

Ignores activity-recognition updates entirely. With this true, the motion state machine falls back to its activity-independent signals only: raw speed/displacement to detect moving, and the stationaryRadius geofence exit to detect departure from a stop. Use this if activity recognition is unreliable or unavailable on a target device class and you’d rather rely purely on GPS-derived motion, accepting a less precise (and typically slower) stop/start detection.

HTTP

BGeo uploads locations natively — a durable SQLite-backed queue drains to your server without Dart in the hot path, and survives app kill/reboot. See the HTTP upload & authorization guide for the full picture, and sync() / getLocations() / getCount() for manual queue inspection.

url

Type: String? · Default: none (persist-only mode)

The endpoint each queued record is POSTed (or method’d) to. Leaving this unset doesn’t disable tracking — it puts the SDK in persist-only mode: locations still queue in SQLite and still reach Dart via onLocation, but nothing is uploaded until you set a url (or call sync() after setting one) yourself.

method

Type: String? · Default: 'POST'

The HTTP verb for uploads: 'POST' | 'PUT' | 'PATCH' (case-insensitive). Any other value — including an empty string or a typo — silently falls back to 'POST' rather than rejecting the config.

headers

Type: Map<String, String>? · Default: {}

Extra headers sent with every upload and log-flush request. If authorization.accessToken is set, it overwrites any Authorization entry you put here; a later token refresh overwrites it again.

params

Type: Map<String, dynamic>? · Default: {}

Merged into the request body root alongside the location payload — useful for constant fields your backend expects on every call (a device ID, an API version). See Shaping the body.

extras

Type: Map<String, dynamic>? · Default: {}

Merged into every uploaded record’s own extras object (a per-call extras — e.g. one passed to insertLocation() — wins over this on key conflicts). Distinct from params, which lands at the body root rather than inside each record.

httpRootProperty

Type: String? · Default: 'location'

The body key that carries the location payload: { "location": { coords: …, timestamp: …, … } } by default, or { "location": [ {…}, {…} ] } once batchSync is uploading more than one record. Set this to "." to merge a single record’s fields directly into the body root instead of nesting them — this only applies when exactly one record is being sent (unset or batchSync: false); a multi-record batch always nests under a real key regardless of this setting.

// Body becomes { coords: {...}, timestamp: '...', is_moving: true, ... }
// instead of { location: { ... } } — useful for a backend with a flat
// per-request schema.
await bg.BackgroundGeolocation.ready(bg.Config(
url: 'https://your-server.example/locations',
httpRootProperty: '.',
));

autoSync

Type: bool? · Default: true

Automatically attempts an upload whenever a new record is queued (subject to autoSyncThreshold and disableAutoSyncOnCellular). Set this false to queue everything and drain it only on your own explicit sync() calls.

autoSyncThreshold

Type: int? · Default: 0

The queue depth that triggers an auto-sync. 0 (the default) fires on every newly-queued record — the least batchy setting. Raising it defers auto-sync until at least this many records are pending, which pairs naturally with batchSync to accumulate a full batch before uploading.

disableAutoSyncOnCellular

Type: bool? · Default: false

Defers auto-sync while the device’s active network is cellular, letting the queue build up until Wi-Fi/ethernet is available (checked on every connectivity change — a device that’s already on cellular when this becomes true also pauses immediately). An explicit sync() call always uploads regardless of network type — this key only gates the automatic path.

batchSync

Type: bool? · Default: false

Uploads multiple queued records in a single request ({ "location": [ {…}, {…} ] }, capped at maxBatchSize) instead of one request per record. Combine with autoSyncThreshold to control how large a batch accumulates before it fires.

// Wait for at least 25 queued records, then upload up to 100 per request.
await bg.BackgroundGeolocation.ready(bg.Config(
url: 'https://your-server.example/locations',
batchSync: true,
maxBatchSize: 100,
autoSyncThreshold: 25,
));

maxBatchSize

Type: int? · Default: -1 (unlimited — sends every pending record in one batch)

Caps how many records a single batchSync request carries. No-op while batchSync is false (every upload is already a single record).

httpTimeoutMs

Type: int? · Units: milliseconds · Default: 30000

Applied uniformly as the connect, read, and write timeout for the upload client. Covers both location uploads and logUrl log-flush requests.

Persistence

See Data pipeline for how queued records flow through the SQLite-backed offline queue.

maxDaysToPersist

Type: int? · Units: days · Default: 2 (engine fallback; the key itself is unset by default)

Prunes queued records older than this many days — unconditionally, on every configure(). The engine hardcodes a 2-day retention window; setting this key only takes effect when the value is positive, in which case it replaces the 2-day fallback (raising or lowering it). Leaving it unset, or setting 0 or a negative number, leaves the engine at its 2-day default — there is no way to disable age-based pruning with this key. In practice this means records older than 2 days are silently pruned from the offline queue unless you explicitly raise maxDaysToPersist. Distinct from logMaxDays, which governs native log retention, not location records.

maxRecordsToPersist

Type: int? · Default: -1 (unlimited)

Caps the offline queue by record count, evicting the oldest rows once the cap is exceeded. A device tracking continuously with no connectivity (and no maxDaysToPersist) will otherwise grow the queue without bound; a real deployment typically sets both this and maxDaysToPersist to keep the SQLite database bounded. See Data pipeline — persistence for the storage details.

Authorization

authorization

Type: Authorization? · Default: none

Configures native, killed-app-safe bearer-token refresh for the upload queue. When the uploader gets a 401/403 on a request, it exchanges the current refresh token for a fresh access token on the native side, with no Dart context required — so a queue draining while your app is killed or suspended doesn’t stall on an expired token until the user happens to reopen the app.

Field (Authorization)TypeDescription
strategyString?'JWT' is the only supported strategy today.
accessTokenString?Sent as Authorization: Bearer <token>, overwriting any Authorization you set in headers.
refreshTokenString?The current refresh token. Native refresh only overwrites this with a genuinely new, non-empty value — a config push that omits it (or resends a stale one) can’t clobber a token the native side already rotated while the app was killed.
refreshUrlString?POST endpoint the refresh request is sent to.
refreshPayloadMap<String, dynamic>?Refresh request body template. Any string value containing the literal {refreshToken} has it substituted with the current refresh token. Default (when unset): { "refresh_token": "<refreshToken>" }.
refreshHeadersMap<String, String>?Extra headers on the refresh request (a JSON Content-Type is added automatically and can’t be overridden here).

The refresh response is parsed tolerantly — { access_token, refresh_token } or { accessToken, refreshToken }, optionally nested under a data key — so it works against a variety of auth backends without a custom adapter.

Refresh outcomes (success or failure, with the HTTP status) surface via the onAuthorization event — see the Events reference and the HTTP guide’s authorization section. Because a background refresh can rotate the tokens while your app isn’t running, prefer getAuthState() over your app’s own persisted copy when resuming in the foreground.

await bg.BackgroundGeolocation.ready(bg.Config(
url: 'https://your-server.example/locations',
authorization: bg.Authorization(
strategy: 'JWT',
accessToken: 'eyJhbGciOi...',
refreshToken: 'a1b2c3...',
refreshUrl: 'https://your-server.example/auth/refresh',
refreshPayload: {'refresh_token': '{refreshToken}'},
refreshHeaders: {'X-Api-Key': '...'},
),
));
bg.BackgroundGeolocation.onAuthorization((event) {
if (event.success) {
// adopt event.accessToken in your own persisted copy
}
});

Application

stopOnTerminate

Type: bool? · Default: false

Whether tracking should stop when the app process is terminated. Honoured on both platforms, but by different mechanisms: Android tears tracking down directly in the foreground service’s onTaskRemoved. iOS has no reliable terminate hook at all, so it’s honoured at next relaunch instead — init drops the persisted enabled flag before any auto-resume path runs, so a relaunch (background or foreground) sees tracking disabled and does not resume it. See the Boot & killed-app behavior guide.

startOnBoot

Type: bool? · Default: false

Whether tracking should resume automatically after a device reboot, if it was enabled when the device went down. Android’s implementation matches its name literally: a BootReceiver restarts the service on BOOT_COMPLETED/LOCKED_BOOT_COMPLETED when both isEnabled() and this flag are true; with false, nothing survives a reboot (geofences, the activity-recognition PendingIntent, the foreground service — all die with the process) until the app is next opened by the user.

iOS has no reboot broadcast to hook, so true here is best-effort by design: there’s no dedicated boot handler, but the wake-region and significant-location-change registrations persist in locationd across a reboot and relaunch the app in the background once — device-verified as roughly a minute after boot, with no movement required. false is still honoured: on relaunch, init detects the reboot (via kern.boottime, tolerant of clock drift, not systemUptime which pauses during device sleep) and drops the persisted enabled flag before any auto-resume path runs, so a later relaunch finds tracking disabled and does nothing.

heartbeatInterval

Type: int? · Units: seconds · Default: 60

How often a heartbeat event fires while tracking is enabled — this timer runs unconditionally alongside tracking, not gated behind a separate opt-in. Each firing carries the last known location and, if the device has appeared stationary-yet-still-moving for a whole interval, nudges the stop-timeout countdown. See the Events reference for the heartbeat payload.

preventSuspend

Type: bool? · Default: false

iOS

Holds a beginBackgroundTask, renewed on every heartbeat, to keep the app process alive a little longer while backgrounded and stationary. No-op on Android, which relies on the (always-on) foreground service instead. Treat this as a minor extension, not a background-execution guarantee — iOS background tasks are time-limited regardless, and this key cannot turn them into a permanent entitlement.

foregroundService

Type: bool? · Default: n/a — has no effect either way

No-op

Accepted for API compatibility but does nothing on either platform: the Android foreground service required for background location runs whenever tracking is enabled, regardless of this key’s value; no-op on iOS, which has no equivalent concept. See Limitations.

notification

Type: Notification? · Default: see table

Android

Configures the Android foreground-service notification shown while tracking is enabled. No-op on iOS, which has no foreground-service notification concept — showsBackgroundLocationIndicator is the closest iOS analogue.

Field (Notification)TypeDefaultDescription
titleString?'Location'Notification title.
textString?'Location tracking active'Notification body text.
channelIdString?'bgeo_location_min'Android notification channel ID.
channelNameString?'Location'User-visible channel name (Settings → App notifications).
smallIconString?app launcher icon"drawable/name" or "mipmap/name" — an invalid or missing resource falls back to the app’s own icon.
colorString?none"#RRGGBB". An unparseable value is logged and ignored (notification keeps the system default tint).
priorityint?-2 (MIN)Transistor-style NOTIFICATION_PRIORITY_* scale: -2 MIN .. 2 MAX.

priority also sets the notification channel’s importance at creation time — and Android freezes a channel’s importance once created; re-posting the notification with a different priority on the same channelId does not change it retroactively. To actually raise or lower importance after the app has run once, change channelId to a new value alongside priority so Android creates a fresh channel.

await bg.BackgroundGeolocation.ready(bg.Config(
notification: bg.Notification(
title: 'Tracking active',
text: 'Your location is being recorded',
channelId: 'bgeo_location_default',
priority: 0, // NotificationCompat.PRIORITY_DEFAULT
),
));

Logging & debug

debug

Type: bool? · Default: false

Plays a one-shot audible cue per event (location, heartbeat, motion-change, stop-timeout start/cancel; geofence events reuse the location cue) on both platforms. Every cue is playable in Debug sound cues, where you can learn which sound means what before taking a device into the field. This is a development convenience only — it does not affect tracking, filtering, or upload behavior in any way, and should never be used to explain a change in GPS behavior.

logLevel

Type: int? · Default: 0 (OFF)

Gates how much is persisted into the native log store (0 OFF .. 5 VERBOSE — see the logLevel* constants). Every log line is still mirrored unconditionally to logcat/os_log regardless of this setting — logLevel only controls what’s additionally kept in the on-device log table for getLog() and optional upload via logUrl. See the Logging & debugging guide.

logMaxDays

Type: int? · Units: days · Default: 3 (coerced up to a minimum of 1)

How long persisted log rows are retained locally before being pruned, on top of a hard 25,000-row cap that also bounds the table regardless of age.

logUrl

Type: String? · Default: none (log rows stay local-only)

Absolute URL for batched native log upload ({ "events": [{ ts, level, src, event, message?, data? }, …] }), using the same headers/authorization/token-refresh machinery as location uploads. Rows are flushed on several triggers (after a location-queue drain, on heartbeat, on app foreground, once 100 rows are pending, or on an explicit uploadLog() call) plus a short foreground-only coalescing delay so a line logged by an idle foregrounded app doesn’t wait for the next heartbeat. A 2xx or 4xx response marks a batch delivered; a 429 (the server throttles log ingestion per device) or 5xx/network failure leaves it pending for the next flush. Uploaded rows aren’t deleted immediately — they remain available to getLog() until logMaxDays/the row cap prunes them.

diagnosticExtras

Type: bool? · Default: false

Test devices only

Adds a compact native diagnostic snapshot (fix counters, app/motion state, active manager configuration) into every uploaded record’s extras. This is a debugging aid for reproducing field issues on instrumented test devices — leave it off for your production fleet, since it adds payload weight and noise to every single location record for no operational benefit.

Geofencing

See addGeofence() / addGeofences() and the Geofencing guide for registering app-facing geofences; these three keys tune how they’re mapped onto the platform’s own (limited) geofence budget.

geofenceProximityRadius

Type: double? · Units: metres · Default: 1000

Of all the geofences you’ve registered with addGeofence(), only those within this radius of the device’s last known fix are actually registered with the OS at any given time (proximity slicing) — see maxMonitoredGeofences for the count cap applied on top of this radius filter. As the device moves, the set of OS-registered geofences is re-evaluated and swapped accordingly.

maxMonitoredGeofences

Type: int? · Default: -1 (use the platform’s own budget as-is: 19 iOS / 99 Android)

A further cap on how many geofences (of those already inside geofenceProximityRadius) are registered with the OS at once. Any value <= 0 leaves the platform’s own hard budget as the only limit; set a smaller positive number to stay further under budget deliberately (e.g. to leave headroom for a separately-managed region).

geofenceInitialTriggerEntry

Type: bool? · Default: true

Requests a synthetic ENTER event for any geofence that’s already-inside at the moment it’s registered with the OS (iOS requestStateForRegion / Android INITIAL_TRIGGER_ENTER), rather than waiting for a genuine boundary crossing that may never come if the device is already inside. Set false if you only want events for geofences actually crossed after registration.