Geofencing
How it works
Geofences let you react when a person enters, stays in, or exits a real-world location—a store, a venue, a neighborhood. You define geofences in Customer.io. The SDK downloads them, monitors them on the device, and automatically sends Geofence Transition events to Customer.io when identified people enter, dwell in, or exit a geofence. You can use these events to trigger automation journeys and build segments, the same way you use any other event.
In Customer.io, you group geofences into geofence sets. A geofence set is a group of geofences that you act on together. For example, you might group all of your stores into a geofence set, and trigger an automation when a device enters any of the geofences in the set. Each Geofence Transition event includes both the geofence crossed and the geofence set it belongs to.
The SDK uses the operating system’s geofencing support so transitions can arrive while your app is in the background (subject to permissions).
Geofencing depends on the Location module, which supplies the device’s current location. This tells the SDK which geofences to monitor.
Prerequisites
Geofencing requires:
- The Location module (a required dependency). See location tracking.
- The Geofence module (
CioLocationGeofence). - Location permission from the person using your app.
- Location services enabled on the device.
Install the geofence module
Add the CioLocation and CioLocationGeofence packages to your project. Geofencing depends on the Location module, so add both. If you use Swift Package Manager, add them the same way you added the other Customer.io packages. They’re part of the customerio-ios package.
Swift Package Manager
.product(name: "Location", package: "customerio-ios")
.product(name: "LocationGeofence", package: "customerio-ios")CocoaPods
pod 'CustomerIO/Location'
pod 'CustomerIO/LocationGeofence'Add location permissions
Add usage descriptions to your Info.plist so iOS can prompt the person for location access:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to send you relevant, location-based messages.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We use your location to send you relevant, location-based messages.</string>The SDK doesn’t request permission on its own. Request it at runtime with CLLocationManager:
import CoreLocation
let locationManager = CLLocationManager()
// When someone first reaches a feature that uses location
locationManager.requestWhenInUseAuthorization()
// Later, when background detection is worth asking for
locationManager.requestAlwaysAuthorization()You want users to Always grant background location permissions
Your audience must set location authorization to Always for geofencing to work the way you probably expect it to. Without this permission—including when your audience has set it to While Using the App—your app can’t detect transitions while it’s closed or in the background; it’s unlikely that your audience has your app open and in the foreground when they pass through a geofence.
| Authorization | What you get |
|---|---|
| Always | Full geofencing support. Geofence transitions arrive when the app is in the foreground, in the background, and whenever iOS relaunches your app. The SDK logs Geofence background delivery active. |
| While Using the App | Geofence transitions only arrive while your app is in the foreground. The SDK logs Geofence registered for foreground delivery only. |
| Denied, Restricted, or not yet asked | Geofencing is effectively disabled. The SDK skips registration and logs Geofence registration skipped: location permission not granted. |
Requesting permissions
iOS doesn’t set the location authorization to Always from a single prompt, so you’ll want to plan your permission flow around it:
- Request when-in-use authorization the first time someone reaches a part of your app where location matters. Explain what they get in return—your
NSLocationWhenInUseUsageDescriptionstring is the only explanation that iOS shows. - Request always authorization later, at a moment when the benefit is obvious—after someone saves a favorite store, or opts into arrival reminders. iOS shows the upgrade prompt on its own schedule, and it may ask the person again whether to keep allowing background location permissions after your app uses location features.
Don’t ask for both permissions when you first launch your app. A person who denies the prompt can only change their mind in Settings, and iOS won’t ask a second time.
You don’t need to tell the SDK when permission changes. It watches for authorization changes and registers geofences on its own when someone grants access—whether they answer a late prompt or change the setting in Settings.
If users turn off precise location, iOS can delay or miss transitions, especially for small geofences.
Initialize the SDK with the geofence module
Register both LocationModule and GeofenceModule when you initialize the SDK. The Location module’s mode controls location tracking only—it doesn’t affect geofencing. By default, the geofence module acquires the location it needs on its own, even when location tracking is off. See location modes below.
import CioDataPipelines
import CioLocation
import CioLocationGeofence
let config = SDKConfigBuilder(cdpApiKey: "your-cdp-api-key")
.addModule(LocationModule(config: LocationConfig(mode: .onAppStart)))
.addModule(GeofenceModule())
.build()
CustomerIO.initialize(withConfig: config)Call initialize on the main thread.
The Geofence Transition event
The SDK sends Geofence Transition events automatically when a person enters, dwells in, or exits a geofence. You don’t need to track these transitions yourself. The transition field in the event tells us whether someone entered, exited, or remained inside a geofence (dwell).
Every geofence belongs to at least one geofence set. When a person crosses a geofence, the SDK sends an event for each geofence set the geofence belongs to, each carrying that set’s geosetId.
The event’s timestamp is when the SDK detected the transition, and the SDK preserves it if delivery is delayed. So a transition captured while the device is offline still lands on the timeline at the moment it occurred.
Events for one visit can arrive late, out of order, or without their matching events, so don’t build automations that require an enter, a dwell, and an exit for every visit.
The properties in each event depend on its transition (enter, dwell, or exit). Choose a transition to see an example event and the properties the SDK sends with it.
Enter
The SDK sends an enter transition when a person crosses into a geofence.
{
"name": "Geofence Transition",
"timestamp": "2026-07-06T17:42:10.482Z",
"properties": {
"transition": "enter",
"geofenceId": "48122",
"geofenceName": "Downtown flagship store",
"geosetId": "25678",
"transitionId": "b3f1c2a4-8e7d-4c2b-9f10-2a6f1e0d5c33",
"metadata": {
"region": "northeast",
"priority": 2
}
}
}- geosetIdstringrequiredCustomer.io-generated ID of the geofence set. Every geofence belongs to at least one set, and the SDK sends one event for each set the geofence belongs to.
- transitionstringrequiredThe person crossed into the geofence.Accepted values:
enter - geofenceIdstringCustomer.io-generated ID of the geofence. This is different from the
external_idused for geofence imports. - geofenceNamestringThe geofence's name. Included only when the geofence has one.
- The metadata you define on the geofence in Customer.io. Values keep their types—a numeric value arrives as a number, not a string. Metadata values must be JSON scalars (
string,number, orboolean); the SDKs drop array and object values. If the geofence has no metadata, the property is an empty object.- additional properties *any type
- transitionIdstringA unique ID for this transition. Events sent for different geofence sets share the same ID, and it stays stable if the SDK retries delivery. To de-duplicate transition events, use
transitionIdandgeosetIdtogether.
Dwell
The SDK sends a dwell transition when a person has stayed inside a geofence for its dwell time. In this example, the geofence’s dwell time is 10 minutes.
A dwell transition means the person stayed inside a geofence for a set amount of time, called the geofence’s dwell time. The SDK sends a dwell transition only for geofences that have a dwell time, and it sends at most one dwell transition per visit.
Dwell events and visit durations start with Android SDK 4.26.0, iOS SDK 4.13.0, React Native 6.16.0, Flutter 4.10.0, and Expo plugin 3.14.0. Older SDK versions only send enter and exit events, without visit properties.
{
"name": "Geofence Transition",
"timestamp": "2026-07-06T17:52:41.204Z",
"properties": {
"transition": "dwell",
"geofenceId": "48122",
"geofenceName": "Downtown flagship store",
"geosetId": "25678",
"transitionId": "e41c7a09-2b5d-4e8f-9c3a-6d1f0b8e2a54",
"visitId": "e41c7a09-2b5d-4e8f-9c3a-6d1f0b8e2a54",
"enteredAt": 1783359730,
"dwellThresholdSeconds": 600,
"dwellDurationSeconds": 631,
"detectionSource": "native",
"metadata": {
"region": "northeast",
"priority": 2
}
}
}- detectionSourcestringrequiredHow the SDK confirmed that the person is still inside the geofence. Use this property for troubleshooting; values vary by platform.
- dwellThresholdSecondsintegerrequiredThe geofence's dwell time, in seconds, when the SDK confirmed the dwell.
- geosetIdstringrequiredCustomer.io-generated ID of the geofence set. Every geofence belongs to at least one set, and the SDK sends one event for each set the geofence belongs to.
- transitionstringrequiredThe person stayed inside the geofence for at least the geofence's dwell time. The SDK only sends one dwell event per visit.Accepted values:
dwell - visitIdstringrequiredAn ID for the visit, from the time the person entered the geofence until they left. If the SDK sends an exit event for this visit, it carries the same
visitId. - dwellDurationSecondsintegerObserved seconds inside the geofence when the SDK confirmed the dwell. This can exceed the dwell time if confirmation was delayed. Rounded up to whole seconds. Included only when the SDK observed the entry and can time the visit reliably.
- enteredAtintegerWhen the person entered the geofence, as a Unix timestamp in seconds. Included only when the SDK knows when the person entered.
- geofenceIdstringCustomer.io-generated ID of the geofence. This is different from the
external_idused for geofence imports. - geofenceNamestringThe geofence's name. Included only when the geofence has one.
- The metadata you define on the geofence in Customer.io. Values keep their types—a numeric value arrives as a number, not a string. Metadata values must be JSON scalars (
string,number, orboolean); the SDKs drop array and object values. If the geofence has no metadata, the property is an empty object.- additional properties *any type
- transitionIdstringA unique ID for this transition. Events sent for different geofence sets share the same ID, and it stays stable if the SDK retries delivery. To de-duplicate transition events, use
transitionIdandgeosetIdtogether.
Exit
The SDK sends an exit transition when a person crosses out of a geofence. When the SDK can time the visit reliably, the exit includes visitDurationSeconds, measured from entry until it detects the exit. Detection can happen after departure; visitDurationUncertaintySeconds describes how much of that duration may be time after the person left. The SDK can also detect the entry after the person arrives, so the actual stay can begin before the recorded entry.
If the SDK didn’t observe the entry, or monitoring was interrupted, the exit omits the duration properties. The SDK tracks visit duration even when the geofence has no dwell time.
{
"name": "Geofence Transition",
"timestamp": "2026-07-06T18:05:41.117Z",
"properties": {
"transition": "exit",
"geofenceId": "48122",
"geofenceName": "Downtown flagship store",
"geosetId": "25678",
"transitionId": "7d2e9b10-4c61-4f3a-a8e5-0b9c3f6d2e71",
"visitId": "e41c7a09-2b5d-4e8f-9c3a-6d1f0b8e2a54",
"enteredAt": 1783359730,
"visitDurationSeconds": 1411,
"visitDurationUncertaintySeconds": 781,
"detectionSource": "native",
"metadata": {
"region": "northeast",
"priority": 2
}
}
}- geosetIdstringrequiredCustomer.io-generated ID of the geofence set. Every geofence belongs to at least one set, and the SDK sends one event for each set the geofence belongs to.
- transitionstringrequiredThe person crossed out of the geofence.Accepted values:
exit - detectionSourcestringHow the SDK confirmed the exit. Use this property for troubleshooting; values vary by platform. Included only when the SDK tracked the visit.
- enteredAtintegerWhen the visit began, as a Unix timestamp in seconds. Included only when the SDK observed the whole visit.
- geofenceIdstringCustomer.io-generated ID of the geofence. This is different from the
external_idused for geofence imports. - geofenceNamestringThe geofence's name. Included only when the geofence has one.
- The metadata you define on the geofence in Customer.io. Values keep their types—a numeric value arrives as a number, not a string. Metadata values must be JSON scalars (
string,number, orboolean); the SDKs drop array and object values. If the geofence has no metadata, the property is an empty object.- additional properties *any type
- transitionIdstringA unique ID for this transition. Events sent for different geofence sets share the same ID, and it stays stable if the SDK retries delivery. To de-duplicate transition events, use
transitionIdandgeosetIdtogether. - visitDurationSecondsintegerSeconds from when the SDK detected the entry until it detected the exit. Detection may happen after departure; see
visitDurationUncertaintySeconds. The SDK can also detect the entry after the person arrives, so the actual stay can begin before the recorded entry. Rounded up to whole seconds. Included only when the SDK can time the visit reliably. - visitDurationUncertaintySecondsintegerHow much of
visitDurationSecondsmay have elapsed after the person left, in seconds. Included only with a recorded duration;0means no additional uncertainty was recorded. - visitIdstringThe ID of the visit this exit ends. A dwell event during the same visit carries the same
visitId. Included only when the SDK tracked the visit.
Location modes and monitoring
Devices can only monitor a limited number of geofences at once, so the SDK monitors the geofences nearest to the user, ranked by distance from the device’s location. The SDK automatically refreshes that set as the device moves. Geofence sets don’t affect monitoring—the SDK ranks individual geofences, and geofence set membership only determines the events it sends.
The geofence module needs a location fix to select that set. The locationMode in GeofenceModuleConfig controls how it gets one:
| Location mode | Behavior |
|---|---|
. (default) | The SDK acquires a location fix on its own whenever geofencing needs one and none is already available—for example, when you identify a person. |
. | The SDK doesn’t acquire location on its own. Call CustomerIO. after you identify someone and they grant location permission. |
.addModule(GeofenceModule(config: GeofenceModuleConfig(locationMode: .automatic)))Location the SDK acquires for geofencing is used for geofencing only—it never produces a Location Update event or updates the person. It also works independently of the Location module’s tracking mode, even when location tracking is .off.
refreshFromCurrentLocation() works in either mode—call it any time you want to force an immediate refresh. Geofences are tied to the identified person, so in .manual mode call it after you identify someone; a refresh requested before you identify anyone isn’t retried automatically.
Neither mode limits detection to the moment the SDK acquires a location. Once the SDK registers geofences, iOS can wake your app near their boundaries, in the background and even between launches, without continuously tracking the person’s location.
Circle and polygon monitoring
The operating system monitors circular geofences directly. For a polygon, the SDK registers the polygon’s enclosing circle with the operating system as a wake-up boundary and uses additional location readings to check movement into or out of the polygon boundary. Depending on the platform and device state, these checks can happen after an operating-system wake, when the app returns to the foreground, or as part of a recovery check.
Geofencing uses background location for background detection, not just for polygons. Because polygon monitoring can request additional location readings while your app is in the background, the operating system may show people a reminder that your app has used or can access their location in the background. The operating system controls the wording and timing of these reminders. Make sure your permission messaging clearly explains why your app needs background location. See Apple’s guidance for requesting location authorization and Android’s guidance for requesting location permissions.
You don’t need to change your SDK setup or event handling to use polygons. The same Geofence module monitors both shapes and sends the same Geofence Transition event.
Polygon support starts with Android SDK 4.22.0, iOS SDK 4.9.0, React Native 6.13.0, Flutter 4.7.0, and Expo plugin 3.11.0 with React Native 6.13.0. Older SDK versions continue to monitor circles, but they don’t receive polygons.
Because polygons require additional location checks, they may use more battery than circles. Background restrictions and location accuracy can delay polygon detection or cause missed enter and exit events. When a circle describes the location well, use a circle for more reliable monitoring with less battery use.
Geofence refresh
The SDK re-downloads your geofence definitions from Customer.io—picking up any geofences you’ve added, changed, or removed, along with the geofence sets they belong to, when:
- Your app launches, or you identify a person—at most once every 24 hours. Geofencing needs an identified person, so if no one is identified when your app launches, the fetch waits until you identify someone.
- The person travels a long distance (about 5 km by default) from where the SDK last fetched.
- Your app forces a refresh by calling
refreshFromCurrentLocation().
So changes you make in Customer.io aren’t instant. They reach a device the next time one of these happens—typically within a day for an app in regular use, but longer for an app no one opens.
Duplicate event suppression
The SDK suppresses duplicate enter and exit events for the same geofence within 1 hour. If a device crosses into a geofence, out again, and then crosses back into the geofence within the next hour, the SDK drops the second entrance event.
Limitations
- The SDK doesn’t continuously track the person’s location.
- Geofencing depends on operating-system callbacks.
- Background delivery depends on platform permissions.
- Geofencing needs the device’s location services. If location is turned off—for example, airplane mode with location disabled—iOS can’t detect crossings until location is available again. Delivery of already-captured transitions resumes when a network connection returns.
- Mobile operating systems limit how many geofences an app can monitor at once. If your app uses geofencing from more than one source (for example, Customer.io and another SDK), all monitored geofences share the same operating-system limit. The Customer.io SDK manages its own geofences within that limit, but it can’t control geofences registered by other SDKs or by your app.