Geofencing
How it works
Geofences let you react when a person enters 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 a Geofence Transition event to Customer.io when your users—represented by profiles in Customer.io—cross into (or out of) a geofence. You can use that event to trigger 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 relies on the operating system’s native geofencing so transitions can arrive while your app is in the background (subject to permissions).
React Native geofencing builds on the native Android and iOS SDKs, and 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, which is enabled automatically when you enable geofencing. See location tracking.
- Location permissions from the person using your app.
- Location services enabled on the device.
- Google Play Services on Android devices.
Enable geofencing in your native projects
Android
Add the following property to your project’s android/gradle.properties file:
customerio_geofence_enabled=true
This also enables the Location module, which geofencing depends on—you don’t need to set customerio_location_enabled separately.
iOS
Add the geofence subspec to your ios/Podfile:
pod "customerio-reactnative", :subspecs => ["apn", "geofence"]
The geofence subspec includes the Location module automatically, so you don’t need to add the location subspec separately.
iOS background bootstrap
iOS may relaunch your app in the background for a geofence transition before the JavaScript runtime has started. To make sure those events are processed, call GeofenceModule.bootstrapForBackgroundDelivery() from your AppDelegate in application(_:didFinishLaunchingWithOptions:):
#if canImport(CioLocationGeofence)
import CioLocationGeofence
#endif
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ...
#if canImport(CioLocationGeofence)
GeofenceModule.bootstrapForBackgroundDelivery(launchOptions: launchOptions)
#endif
// ...
}
Location permissions
Your app requests location permission itself—the SDK never prompts. What the person grants determines whether you detect crossings while your app is closed, so read both platform sections before you design the permission flow.
iOS
Add usage descriptions to your ios/<YourApp>/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>
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 on iOS
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.
Android
Add the following permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
You want background location permissions
Geofencing requires your user to grant location permissions. In general, you want your users to set Allow all the time for the background location permission, so that your app can detect geofence transitions when your app is closed or in the background.
While ACCESS_FINE_LOCATION alone is enough to register geofences, and that may give the appearance that geofencing is working, Android only delivers transitions while your app is in the foreground. Since people rarely have your app open at the moment they arrive somewhere, foreground-only access will miss most geofence transitions.
| Permission | Behavior if missing |
|---|---|
ACCESS_FINE_LOCATION | Geofencing is effectively disabled. The SDK can’t register geofences, and logs Cannot register geofences: ACCESS_FINE_LOCATION not granted. Coarse location isn’t a substitute. |
ACCESS_BACKGROUND_LOCATION | Transitions arrive only while your app is in the foreground. The SDK logs ACCESS_BACKGROUND_LOCATION not granted — transitions will only fire while the app is in the foreground. |
RECEIVE_BOOT_COMPLETED | Geofences aren’t restored after a reboot until the person opens your app again. |
Background location is a separate grant on Android 10 (API level 29) and later. On earlier versions, ACCESS_FINE_LOCATION covers background delivery on its own.
Requesting permissions
On Android 11 (API level 30) and later, you can’t request access to background location in the same prompt as foreground location. That means you need to ask for background location in two steps.
- Request
ACCESS_FINE_LOCATIONwhen someone reaches a part of your app where location matters, and explain what they get in return. - Request
ACCESS_BACKGROUND_LOCATIONlater, at a moment when the benefit is obvious. On Android 11 and later, this takes the person to system settings, where they choose Allow all the time themselves—so tell them what to pick before you send them there.
Call refreshFromCurrentLocation() when the user grants the background location permission, so the SDK picks up nearby geofences without waiting for its next scheduled refresh.
If the user downgrades to foreground-only access later, the SDK keeps its registrations but the operating system won’t wake your app when transitioning in or out of a geofence.
Google Play also requires a declaration and review for apps that request background location. You should plan to spend time on this before you release your app.
Initialize the SDK with geofencing
Add a geofence object to your initialization config. Providing it opts your app into geofence monitoring and enables the Location module automatically—you don’t need a separate location config. By default, the geofence module acquires the location it needs on its own. See location modes below.
CustomerIO.initialize({
cdpApiKey: 'your-cdp-api-key',
region: CioRegion.US,
geofence: {},
});
Optional: background delivery on iOS
By default, when you enable geofencing, the SDK stores your cdpApiKey on the device so geofence events triggered from a background cold start are delivered in real time. Most apps don’t need to change this. To set it explicitly, pass an ios object:
CustomerIO.initialize({
cdpApiKey: 'your-cdp-api-key',
region: CioRegion.US,
geofence: {},
ios: {
allowBackgroundDelivery: true,
},
});
When allowBackgroundDelivery is off, events are queued and delivered the next time your app enters the foreground.
The Geofence Transition event
The SDK sends the Geofence Transition event automatically when a person enters or exits a geofence. You don’t need to track enter or exit transitions yourself.
When a person crosses a geofence, the SDK sends one event for each geofence set the geofence belongs to, each carrying that set’s geosetId. A geofence that isn’t in any geofence set produces a single event without a geosetId.
The event’s timestamp reflects when the user’s device crossed into or out of a geofence, not when the SDK delivered the event. A transition captured while the device isn’t connected to the network still lands on the timeline at the moment it occurred.
| Property | Description |
|---|---|
transition | The direction the device crossed the geofence boundary—enter or exit. |
geofenceId | Customer.io-generated ID of the geofence. This is different from the external_id used for imports. |
geofenceName | The geofence’s name. Included only when the geofence has one. |
geosetId | Customer.io-generated ID of the geofence set. |
metadata | The metadata you define on the geofence in Customer.io. Values keep their types—a numeric value arrives as a number, not a string. If the geofence has no metadata, the property is an empty object. |
transitionId | A unique ID for the transition into or out of the geofence. 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 transitionId and geosetId together. |
Example event:
{
"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
}
}
}
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 your geofence config controls how it gets one:
| Location mode | Behavior |
|---|---|
CioGeofenceLocationMode.Automatic (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 profile. |
CioGeofenceLocationMode.Manual | The SDK doesn’t acquire location on its own. Call CustomerIO.geofence.refreshFromCurrentLocation() after you identify someone and they grant location permission. |
CustomerIO.initialize({
cdpApiKey: 'your-cdp-api-key',
region: CioRegion.US,
geofence: {
locationMode: CioGeofenceLocationMode.Automatic,
},
});
Location the SDK acquires for geofencing is used for geofencing only—it never produces a Location Update event or updates the profile. 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 profile, 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, the operating system monitors them natively—it wakes your app when the device crosses one, in the background and even between launches, without continuously tracking their location.
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 profile, 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 profile’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—the operating system can’t detect crossings until location is available again. Delivery of already-captured transitions resumes when a network connection returns.
- Android geofencing requires Google Play Services.
- 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.